diff --git a/README.md b/README.md index 56cd92c4f3..63cb896233 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ Some of the features of OpenPDF include: * Table Support: The library facilitates the creation of tables in PDF documents. * Encryption: You can encrypt PDF documents for security purposes. * Page Layout: OpenPDF allows you to set the page size, orientation, and other layout properties. +* Convert PDF files to images using [openpdf-pdfrenderer](openpdf-pdfrenderer). ## Use OpenPDF as Maven dependency @@ -69,6 +70,8 @@ Add this to your pom.xml file to use the latest version of OpenPDF: You can find also a nice explanation of these licenses under https://itsfoss.com/open-source-licenses-explained/ +openpdf-pdfrenderer is licensed with GNU Lesser General Public License (LGPL), Version 2.1 only. + We want OpenPDF to consist of source code which is consistently licensed with the LGPL and MPL licences only. This also means that any new contributions to the project must have a dual LGPL and MPL license only. diff --git a/changelogs/2.0.4.md b/changelogs/2.0.4.md new file mode 100644 index 0000000000..bf9dc15cd9 --- /dev/null +++ b/changelogs/2.0.4.md @@ -0,0 +1,7 @@ +# 2.0.4 + +## Changes + +* Add openpdf-pdfrenderer for exporting PDF files as images. It is a fork of https://github.com/katjas/PDFrenderer +* Add RegionPdfTextExtractor for extracting text on a specific part of a page in a PDF document. +* Add test case for LibrePDF#1199 r = 5 encryption. \ No newline at end of file diff --git a/openpdf-pdfrenderer/LICENSE.txt b/openpdf-pdfrenderer/LICENSE.txt new file mode 100644 index 0000000000..cc4078c9b2 --- /dev/null +++ b/openpdf-pdfrenderer/LICENSE.txt @@ -0,0 +1,170 @@ +The GNU Lesser General Public License, version 2.1 (LGPL-2.1) +[OSI Approved License] +GNU Lesser General Public License + +Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + + [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] + +Preamble + +The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. + +This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. + +When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. + +To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. + +We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. + +Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. + +Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. + +When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. + +We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. + +For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. + +In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. + +Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. + +The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". + +A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. + +You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. + + (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) + + These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. + + Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. + + In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. + +3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. + +However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. + +When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. + +If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. + +6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: + + a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. + + e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. + +It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. + + b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. + +This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. + +NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +END OF TERMS AND CONDITIONS +How to Apply These Terms to Your New Libraries +If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). + +To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + Copyright (C) + + This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + signature of Ty Coon, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! \ No newline at end of file diff --git a/openpdf-pdfrenderer/README.md b/openpdf-pdfrenderer/README.md new file mode 100644 index 0000000000..3cab63b64c --- /dev/null +++ b/openpdf-pdfrenderer/README.md @@ -0,0 +1,99 @@ +openpdf-pdfrenderer +=================== + +PDF to Image renderer for OpenPDF. + +Because every fine PDF library should be able to create a PNG image from a PDF page. + +License: GNU Lesser General Public License + +--- + +This is a fork of https://github.com/katjas/PDFrenderer which is based on [pdf-renderer](http://java.net/projects/pdf-renderer) (covered by the LGPL-2.1 license) for improvement purposes. + +The principal objective of the fork is to improve the original PDF renderer. The original version is able to handle most of the PDF 1.4 features, but has several bugs and missing functionality. + + + +To do: +------ +* some colours are displayed incorrect, there seem to be open issues regarding colour space handling +* some fonts can't be rendered and are replaced with built in fonts +* embedded Type0 font with a CIDType0 font is not supported correctly. Currently there is a hack in the code to fall back to the built in fonts in this case. +* try to improve support of auto adjust stroke and overprint mode - the data is read but not really handled correctly. + +Done: +----- +* support for widget annotation containing digital signature +* support function type 4 rendering (subset of the PostScript language, specification taken from http://www.adobe.com/devnet/acrobat/pdfs/adobe_supplement_iso32000.pdf) +* support link annotations for being able to render links +* rough support of stamp and freetext annotations +* handle alternate colour spaces (colour space plus a function to be applied on the colour values) +* fixes transparency issues / transparent masked images (even though transparency is still not completely supported) +* corrected handling of overlapping shapes +* better support Type0 fonts that use embedded CID fonts +* jbig2 image format decoded with (improved) "jpedal" API +* DeviceCMY / DeviceRGB colour spaces are working now, but some PDFs are still displayed in wrong format. +* Improved reading of CMYK images. Some colours are still displayed wrong. (using the ch.randelshofer.media.jpeg.JPEGImageIO API) +* Improved run length decoding (corrected reading of buffer) +* fixed compression issues +* fixed size of outline fonts +* fixed several exceptions +* Fixed various font encoding problems (Flex in Type 1, wrong stemhints in Type 1C and inverted presentation of Type 3) +* fixed rotation of text (http://java.net/jira/browse/PDF_RENDERER-91) +* JPEG decoding with imageio +* Work-around lack of YCCK decoding support in standard JRE image readers and thus allow CMYK jpeg images without using 3rd party image readers (e.g., JAI) +* Employ local TTF files if available instead of using the built-ins as substitutes. Scanning of available TTFs will take some time on the first request for an unavailable TTF. This behaviour can be disabled by setting the system property PDFRenderer.avoidExternalTtf to true. The PDFRenderer.fontSearchPath system property can be used to alter the search path, though Windows and Mac OS X defaults should hopefully be sensible. +* Added TIFF Type 2 Predictor for decoding +* use built in font as workaround for MMType1 fonts instead of throwing an exception +* introduced configuration options for improving the memory usage when rendering PDFs with large (e.g. scanned) images +* improved parsing of SMask images +* modified parsing of paths, some closures were missing +* added some debugging +* Add option to inject exception handling - e.g. for redirecting the stack trace to a log file +* Add SymbolSetEncoding + +Usage / Example +------- + +// example class for displaying a PDF file +```java +public class PDFDisplay extends JComponent{ + + // byte array containing the PDF file content + private byte[] bytes = null; + + + // some more code + + @Override + public void paintComponent(Graphics g) { + int pageindex = 1; + PDFFile pdfFile = new PDFFile(ByteBuffer.wrap(this.bytes)); + PDFPage page = pdfFile.getPage(pageIndex); + Paper paper = new Paper(); + int formatOrientation = page.getAspectRatio() > 1 ? PageFormat.LANDSCAPE + : PageFormat.PORTRAIT; + if(formatOrientation == PageFormat.LANDSCAPE) { + paper.setSize(page.getHeight(), page.getWidth()); + }else { + paper.setSize(page.getWidth(), page.getHeight()); + } + PageFormat pageFormat = new PageFormat(); + pageFormat.setPaper(paper); + pageFormat.setOrientation(formatOrientation); + + Graphics2D g2d = (Graphics2D)g.create(); + Rectangle imgbounds = new Rectangle(0, 0, (int)pageFormat.getWidth(), + (int)pageFormat.getHeight()); + PDFRenderer renderer = new PDFRenderer(page, g2d, imgbounds, null, Color.WHITE); + try { + this.page.waitForFinish(); + } + catch (InterruptedException e) { + // some exception handling + } + renderer.run(); + } +} +``` diff --git a/openpdf-pdfrenderer/pom.xml b/openpdf-pdfrenderer/pom.xml new file mode 100644 index 0000000000..3ed4c15368 --- /dev/null +++ b/openpdf-pdfrenderer/pom.xml @@ -0,0 +1,48 @@ + + + + com.github.librepdf + openpdf-parent + 2.0.4-SNAPSHOT + + + 4.0.0 + openpdf-pdfrenderer + openpdf-pdfrenderer + + + + Lesser General Public License (LGPL) + http://www.gnu.org/copyleft/lesser.html + + + PDF renderer implementation supporting the subset of PDF 1.4 specification. + + + + + com.github.librepdf + openpdf + ${project.version} + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.assertj + assertj-core + test + + + + \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/BaseWatchable.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/BaseWatchable.java new file mode 100644 index 0000000000..34f7525f4d --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/BaseWatchable.java @@ -0,0 +1,398 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer; + +/** + * An abstract implementation of the watchable interface, that is extended + * by the parser and renderer to do their thing. + */ +public abstract class BaseWatchable implements Watchable, Runnable { + + /** the current status, from the list in Watchable */ + private int status = Watchable.UNKNOWN; + /** a lock for status-related operations */ + private final Object statusLock = new Object(); + /** a lock for parsing operations */ + private final Object parserLock = new Object(); + /** when to stop */ + private Gate gate; + /** suppress local stack trace on setError. */ + private static boolean SuppressSetErrorStackTrace = false; + /** the thread we are running in */ + private Thread thread; + private Exception exception; + + // handle exceptions via this class + private static PDFErrorHandler errorHandler = new PDFErrorHandler(); + + /** + * Creates a new instance of BaseWatchable + */ + protected BaseWatchable() { + setStatus(Watchable.NOT_STARTED); + } + + /** + * Perform a single iteration of this watchable. This is the minimum + * granularity which the go() commands operate over. + * + * @return one of three values:
    + *
  • Watchable.RUNNING if there is still data to be processed + *
  • Watchable.NEEDS_DATA if there is no data to be processed but + * the execution is not yet complete + *
  • Watchable.COMPLETED if the execution is complete + *
+ */ + protected abstract int iterate() throws Exception; + + /** + * Prepare for a set of iterations. Called before the first iterate() call + * in a sequence. Subclasses should extend this method if they need to do + * anything to setup. + */ + protected void setup() { + // do nothing + } + + /** + * Clean up after a set of iterations. Called after iteration has stopped + * due to completion, manual stopping, or error. + */ + protected void cleanup() { + // do nothing + } + + @Override + public void run() { + try { + Thread.sleep(1); + // call setup once we started + if (getStatus() == Watchable.NOT_STARTED) { + setup(); + } + + setStatus(Watchable.PAUSED); + + synchronized (this.parserLock) { + while (!isFinished() && getStatus() != Watchable.STOPPED) { + if (isExecutable()) { + // set the status to running + setStatus(Watchable.RUNNING); + + try { + // keep going until the status is no longer running, + // our gate tells us to stop, or no-one is watching + int laststatus = Watchable.RUNNING; + while ((getStatus() == Watchable.RUNNING) && (this.gate == null || !this.gate.iterate())) { + // update the status based on this iteration + int status = iterate(); + if (status != laststatus) { + // update status only when necessary, this increases performance + setStatus(status); + laststatus = status; + } + + } + + // make sure we are paused + if (getStatus() == Watchable.RUNNING) { + setStatus(Watchable.PAUSED); + } + } catch (Exception ex) { + setError(ex); + } + } else { + // wait for our status to change + synchronized (this.statusLock) { + if (!isExecutable()) { + try { + this.statusLock.wait(); + } catch (InterruptedException ie) { + // ignore + } + } + } + } + } + } + // call cleanup when we are done + if (getStatus() == Watchable.COMPLETED || getStatus() == Watchable.ERROR) { + + cleanup(); + } + } catch (InterruptedException e) { + PDFDebugger.debug("Interrupted."); + } + // notify that we are no longer running + this.thread = null; + } + + /** + * Get the status of this watchable + * + * @return one of the well-known statuses + */ + @Override + public int getStatus() { + return this.status; + } + + /** + * Return whether this watchable has finished. A watchable is finished + * when its status is either COMPLETED, STOPPED or ERROR + */ + public boolean isFinished() { + int s = getStatus(); + return (s == Watchable.COMPLETED || + s == Watchable.ERROR); + } + + /** + * return true if this watchable is ready to be executed + */ + public boolean isExecutable() { + return ((this.status == Watchable.PAUSED || this.status == Watchable.RUNNING) && + (this.gate == null || !this.gate.stop())); + } + + /** + * Stop this watchable if it is not already finished. + * Stop will cause all processing to cease, + * and the watchable to be destroyed. + */ + @Override + public void stop() { + if (!isFinished()) setStatus(Watchable.STOPPED); + } + + /** + * Start this watchable and run in a new thread until it is finished or + * stopped. + * Note the watchable may be stopped if go() with a + * different time is called during execution. + */ + @Override + public synchronized void go() { + this.gate = null; + + execute(false); + } + + /** + * Start this watchable and run until it is finished or stopped. + * Note the watchable may be stopped if go() with a + * different time is called during execution. + * + * @param synchronous if true, run in this thread + */ + public synchronized void go(boolean synchronous) { + this.gate = null; + + execute(synchronous); + } + + /** + * Start this watchable and run for the given number of steps or until + * finished or stopped. + * + * @param steps the number of steps to run for + */ + @Override + public synchronized void go(int steps) { + this.gate = new Gate(); + this.gate.setStopIterations(steps); + + execute(false); + } + + /** + * Start this watchable and run for the given amount of time, or until + * finished or stopped. + * + * @param millis the number of milliseconds to run for + */ + @Override + public synchronized void go(long millis) { + this.gate = new Gate(); + this.gate.setStopTime(millis); + + execute(false); + } + + /** + * Wait for this watchable to finish + */ + public void waitForFinish() { + synchronized (this.statusLock) { + while (!isFinished() && getStatus() != Watchable.STOPPED) { + try { + this.statusLock.wait(); + } catch (InterruptedException ex) { + // ignore + } + } + } + } + + /** + * Start executing this watchable + * + * @param synchronous if true, run in this thread + */ + protected synchronized void execute(boolean synchronous) { + // see if we're already running + if (this.thread != null) { + // we're already running. Make sure we wake up on any change. + synchronized (this.statusLock) { + this.statusLock.notifyAll(); + } + + return; + } else if (isFinished()) { + // we're all finished + return; + } + + // we'return not running. Start up + if (synchronous) { + this.thread = Thread.currentThread(); + run(); + } else { + this.thread = new Thread(this); + this.thread.setName(getClass().getName()); + //Fix for NPE: Taken from http://java.net/jira/browse/PDF_RENDERER-46 + synchronized (statusLock) { + Thread.UncaughtExceptionHandler h = new Thread.UncaughtExceptionHandler() { + @Override + public void uncaughtException( Thread th, Throwable ex ) + { + PDFDebugger.debug( "Uncaught exception: " + ex ); + } + }; + thread.setUncaughtExceptionHandler( h ); + thread.start(); + try { + statusLock.wait(); + } catch (InterruptedException ex) { + // ignore + } + } + } + } + + /** + * Set the status of this watchable + */ + protected void setStatus(int status) { + synchronized (this.statusLock) { + this.status = status; + + this.statusLock.notifyAll(); + } + } + + /** + * return true if we would be suppressing setError stack traces. + * + * @return boolean + */ + public static boolean isSuppressSetErrorStackTrace () { + return SuppressSetErrorStackTrace; + } + + /** + * set suppression of stack traces from setError. + * + * @param suppressTrace + */ + public static void setSuppressSetErrorStackTrace(boolean suppressTrace) { + SuppressSetErrorStackTrace = suppressTrace; + } + + /** + * Set an error on this watchable + */ + protected void setError(Exception error) { + exception = error; + if (!SuppressSetErrorStackTrace) { + errorHandler.publishException(error); + } + + setStatus(Watchable.ERROR); + } + + public Exception getException() { + return exception; + } + + /** A class that lets us give it a target time or number of steps, + * and will tell us to stop after that much time or that many steps + */ + static class Gate { + + /** whether this is a time-based (true) or step-based (false) gate */ + private boolean timeBased; + /** the next gate, whether time or iterations */ + private long nextGate; + + /** set the stop time */ + public void setStopTime(long millisFromNow) { + this.timeBased = true; + this.nextGate = System.currentTimeMillis() + millisFromNow; + } + + /** set the number of iterations until we stop */ + public void setStopIterations(int iterations) { + this.timeBased = false; + this.nextGate = iterations; + } + + /** check whether we should stop. + */ + public boolean stop() { + if (this.timeBased) { + return (System.currentTimeMillis() >= this.nextGate); + } else { + return (this.nextGate < 0); + } + } + + /** Notify the gate of one iteration. Returns true if we should + * stop or false if not + */ + public boolean iterate() { + if (!this.timeBased) { + this.nextGate--; + } + + return stop(); + } + } + + public static void setErrorHandler(PDFErrorHandler e) { + errorHandler = e; + } + + public static PDFErrorHandler getErrorHandler(){ + if(errorHandler == null) { + errorHandler = new PDFErrorHandler(); + } + return errorHandler; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/ByteBufferInputStream.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/ByteBufferInputStream.java new file mode 100644 index 0000000000..55f244e7d1 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/ByteBufferInputStream.java @@ -0,0 +1,107 @@ +/* + * Copyright 2010 Pirion Systems Pty Ltd, 139 Warry St, + * Fortitude Valley, Queensland, Australia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * Exposes a {@link ByteBuffer} as an {@link InputStream}. + * + * @author Luke Kirby + */ +public class ByteBufferInputStream extends InputStream { + + /** The underlying byte buffer */ + private ByteBuffer buffer; + + /** + * Class constructor + * @param buffer the buffer to present as an input stream, positioned + * at the current read position of the byte buffer + */ + public ByteBufferInputStream(ByteBuffer buffer) { + this.buffer = buffer; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + + if (b == null) { + throw new NullPointerException(); + } else if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return 0; + } + + final int remaining = buffer.remaining(); + if (remaining == 0) { + return -1; + } else if (remaining < len) { + buffer.get(b, off, remaining); + return remaining; + } else { + buffer.get(b, off, len); + return len; + } + } + + @Override + public long skip(long n) throws IOException { + if (n <= 0) { + return 0; + } else { + final int remaining = buffer.remaining(); + if (n < remaining) { + buffer.position(buffer.position() + remaining); + return remaining; + } else { + buffer.position((int) (buffer.position() + n)); + return n; + } + } + } + + @Override + public int read() throws IOException { + return buffer.get(); + } + + @Override + public int available() throws IOException { + return buffer.remaining(); + } + + @Override + public void mark(int readlimit) { + buffer.mark(); + } + + @Override + public boolean markSupported() { + return true; + } + + @Override + public void reset() throws IOException { + buffer.reset(); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/Cache.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/Cache.java new file mode 100644 index 0000000000..e0bf4b92f5 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/Cache.java @@ -0,0 +1,311 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer; + +import java.awt.image.BufferedImage; +import java.lang.ref.SoftReference; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * A cache of PDF pages and images. + */ +public class Cache { + + /** the pages in the cache, mapped by page number */ + private final Map> pages; + + /** Creates a new instance of a Cache */ + public Cache() { + this.pages = Collections.synchronizedMap(new HashMap>()); + } + + /** + * Add a page to the cache. This method should be used for + * pages which have already been completely rendered. + * + * @param pageNumber the page number of this page + * @param page the page to add + */ + public void addPage(Integer pageNumber, PDFPage page) { + addPageRecord(pageNumber, page, null); + } + + /** + * Add a page to the cache. This method should be used for + * pages which are still in the process of being rendered. + * + * @param pageNumber the page number of this page + * @param page the page to add + * @param parser the parser which is parsing this page + */ + public void addPage(Integer pageNumber, PDFPage page, PDFParser parser) { + addPageRecord(pageNumber, page, parser); + } + + /** + * Add an image to the cache. This method should be used for images + * which have already been completely rendered + * + * @param page page this image is associated with + * @param info the image info associated with this image + * @param image the image to add + */ + public void addImage(PDFPage page, ImageInfo info, BufferedImage image) { + addImageRecord(page, info, image, null); + } + + /** + * Add an image to the cache. This method should be used for images + * which are still in the process of being rendered. + * + * @param page the page this image is associated with + * @param info the image info associated with this image + * @param image the image to add + * @param renderer the renderer which is rendering this page + */ + public void addImage(PDFPage page, ImageInfo info, BufferedImage image, + PDFRenderer renderer) { + addImageRecord(page, info, image, renderer); + } + + /** + * Get a page from the cache + * + * @param pageNumber the number of the page to get + * @return the page, if it is in the cache, or null if not + */ + public PDFPage getPage(Integer pageNumber) { + PageRecord rec = getPageRecord(pageNumber); + if (rec != null) { + return (PDFPage) rec.value; + } + + // not found + return null; + } + + /** + * Get a page's parser from the cache + * + * @param pageNumber the number of the page to get the parser for + * @return the parser, or null if it is not in the cache + */ + public PDFParser getPageParser(Integer pageNumber) { + PageRecord rec = getPageRecord(pageNumber); + if (rec != null) { + return (PDFParser) rec.generator; + } + + // not found + return null; + } + + /** + * Get an image from the cache + * + * @param page the page the image is associated with + * @param info the image info that describes the image + * + * @return the image if it is in the cache, or null if not + */ + public BufferedImage getImage(PDFPage page, ImageInfo info) { + Record rec = getImageRecord(page, info); + if (rec != null) { + return (BufferedImage) rec.value; + } + + // not found + return null; + } + + /** + * Get an image's renderer from the cache + * + * @param page the page this image was generated from + * @param info the image info describing the image + * @return the renderer, or null if it is not in the cache + */ + public PDFRenderer getImageRenderer(PDFPage page, ImageInfo info) { + Record rec = getImageRecord(page, info); + if (rec != null) { + return (PDFRenderer) rec.generator; + } + + // not found + return null; + } + + /** + * Remove a page and all its associated images, as well as its parser + * and renderers, from the cache + * + * @param pageNumber the number of the page to remove + */ + public void removePage(Integer pageNumber) { + removePageRecord(pageNumber); + } + + /** + * Remove an image and its associated renderer from the cache + * + * @param page the page the image is generated from + * @param info the image info of the image to remove + */ + public void removeImage(PDFPage page, ImageInfo info) { + removeImageRecord(page, info); + } + + /** + * The internal routine to add a page to the cache, and return the + * page record which was generated + */ + PageRecord addPageRecord(Integer pageNumber, PDFPage page, + PDFParser parser) { + PageRecord rec = new PageRecord(); + rec.value = page; + rec.generator = parser; + + this.pages.put(pageNumber, new SoftReference(rec)); + + return rec; + } + + /** + * Get a page's record from the cache + * + * @return the record, or null if it's not in the cache + */ + PageRecord getPageRecord(Integer pageNumber) { + PDFDebugger.debug("Request for page " + pageNumber, 1000); + SoftReference ref = this.pages.get(pageNumber); + if (ref != null) { + String val = (ref.get() == null) ? " not in " : " in "; + PDFDebugger.debug("Page " + pageNumber + val + "cache", 1000); + return ref.get(); + } + + PDFDebugger.debug("Page " + pageNumber + " not in cache", 1000); + // not in cache + return null; + } + + /** + * Remove a page's record from the cache + */ + PageRecord removePageRecord(Integer pageNumber) { + SoftReference ref = this.pages.remove(pageNumber); + if (ref != null) { + return ref.get(); + } + + // not in cache + return null; + } + + /** + * The internal routine to add an image to the cache and return the + * record that was generated. + */ + Record addImageRecord(PDFPage page, ImageInfo info, + BufferedImage image, PDFRenderer renderer) { + // first, find or create the relevant page record + Integer pageNumber = Integer.valueOf(page.getPageNumber()); + PageRecord pageRec = getPageRecord(pageNumber); + if (pageRec == null) { + pageRec = addPageRecord(pageNumber, page, null); + } + + // next, create the image record + Record rec = new Record(); + rec.value = image; + rec.generator = renderer; + + // add it to the cache + pageRec.images.put(info, new SoftReference(rec)); + + return rec; + } + + /** + * Get an image's record from the cache + * + * @return the record, or null if it's not in the cache + */ + Record getImageRecord(PDFPage page, ImageInfo info) { + // first find the relevant page record + Integer pageNumber = Integer.valueOf(page.getPageNumber()); + + PDFDebugger.debug("Request for image on page " + pageNumber, 1000); + + PageRecord pageRec = getPageRecord(pageNumber); + if (pageRec != null) { + SoftReference ref = pageRec.images.get(info); + if (ref != null) { + String val = (ref.get() == null) ? " not in " : " in "; + PDFDebugger.debug("Image on page " + pageNumber + val + " cache", 1000); + return ref.get(); + } + } + + PDFDebugger.debug("Image on page " + pageNumber + " not in cache", 1000); + // not found + return null; + } + + /** + * Remove an image's record from the cache + */ + Record removeImageRecord(PDFPage page, ImageInfo info) { + // first find the relevant page record + Integer pageNumber = Integer.valueOf(page.getPageNumber()); + PageRecord pageRec = getPageRecord(pageNumber); + if (pageRec != null) { + SoftReference ref = pageRec.images.remove(info); + if (ref != null) { + return ref.get(); + } + + } + + return null; + } + + /** the basic information about a page or image */ + class Record { + + /** the page or image itself */ + Object value; + /** the thing generating the page, or null if done/not provided */ + BaseWatchable generator; + } + + /** the record stored for each page in the cache */ + class PageRecord extends Record { + + /** any images associated with the page */ + Map> images; + + /** create a new page record */ + public PageRecord() { + this.images = Collections.synchronizedMap(new HashMap>()); + } + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/Configuration.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/Configuration.java new file mode 100644 index 0000000000..4c1d5c17cf --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/Configuration.java @@ -0,0 +1,207 @@ +package com.github.librepdf.pdfrenderer; + +/** + * Since there is no context that is passed between the various classes that + * perform the pdf parsing and rendering, we introduce this class to at least + * globally configure PDFRenderer. + * + * Typically you would configure the global instance before using any other + * PDFRenderer API. + */ +public class Configuration { + private static Configuration INSTANCE; + + /** whether grey scale images will be converted to ARGB */ + private boolean convertGreyscaleImagesToArgb = true; + /** threshold in pixels after which images are rendered in chunks (disabled by default) */ + private int thresholdForBandedImageRendering = 0; + /** whether color convert op should be used or not for parsing an image*/ + private boolean avoidColorConvertOp = false; + /** Use blur before image resize to enhance the result (Antialias) **/ + private boolean useBlurResizingForImages = true; + + /** Print signature fields on pdf **/ + private boolean printSigantureFields = true; + /** Print stamp annotations on pdf **/ + private boolean printStampAnnotations = true; + /** Print widget annotations on pdf **/ + private boolean printWidgetAnnotations = true; + /** Print freetext annotations on pdf **/ + private boolean printFreetextAnnotations = true; + /** Print link annotations on pdf **/ + private boolean printLinkAnnotations = true; + + public static synchronized Configuration getInstance() { + if (INSTANCE == null) { + INSTANCE = new Configuration(); + } + return INSTANCE; + } + + /** + * Enables or disables the conversion of greyscale images to ARGB. + * Disabling this may have a lower memory overhead with high resolution + * (e.g. scanned) images. Note that this has to be called before + * {@link #getImage()} is called to have an effect. + * + * Enabled by default. + * @param aFlag whether greyscale images shall be converted to ARGB. + */ + public void setConvertGreyscaleImagesToArgb(boolean aFlag) { + convertGreyscaleImagesToArgb = aFlag; + } + + /** + * Returns true if greyscale images will be converted to ARGB + */ + public boolean isConvertGreyscaleImagesToArgb() { + return convertGreyscaleImagesToArgb; + } + + /** + * If an image is higher than the given size (in pixels) then + * the image will be rendered in chunks, rather than as one big image. + * This may lead to lower memory consumption for e.g. scanned PDFs with + * large images. + * + * Set to a value <= 0 to disable banded image rendering. + * Defaults to 0 (off) + * + * @param aSize the height threshold at which to enable banded image rendering + */ + public void setThresholdForBandedImageRendering(int aSize) { + thresholdForBandedImageRendering = aSize; + } + + /** + * Returns the image height threshold at which to enable banded image rendering. + * @return the threshold value, or a value <= 0 if banded rendering is disabled + */ + public int getThresholdForBandedImageRendering() { + return thresholdForBandedImageRendering; + } + + /** + * Is the color converting op switched on or off? + * @return - the usage of this color convert op + */ + public boolean isAvoidColorConvertOp() { + return avoidColorConvertOp; + } + + /** + * Set this to false to switch off the + * use of this color convert op which may segfault on some platforms + * due to a variety of problems related to thread safety and + * the native cmm library underlying this conversion op, e.g., + * https://forums.oracle.com/forums/thread.jspa?threadID=1261882&;tstart=225&messageID=5356357 + * (Unix platforms seem the most affected) + * + * If the system is bug-free, though, this does make use + * of native libraries and sees a not insignificant speed-up, + * though it's still not exactly fast. If we don't run this op + * now, it's performed at some later stage, but without using + * the native code + * @param avoidColorConvertOp + */ + public void setAvoidColorConvertOp(boolean avoidColorConvertOp) { + this.avoidColorConvertOp = avoidColorConvertOp; + } + + /** + * Use blur before image resize to enhance the result (Antialias)? + * @return the useBlurResizingForImages + */ + public boolean isUseBlurResizingForImages() { + return useBlurResizingForImages; + } + + /** + * Use blur before image resize to enhance the result (Antialias) + * + * @param useBlurResizingForImages + */ + public void setUseBlurResizingForImages(boolean useBlurResizingForImages) { + this.useBlurResizingForImages = useBlurResizingForImages; + } + + /** + * Print signature fields on pdf + * + * @param printSignatureField + */ + public void setPrintSignatureFields(boolean printSignatureFields) { + this.printSigantureFields = printSignatureFields; + } + + /** + * @return true if signature fields will be printed on pdf + */ + public boolean isPrintSignatureFields() { + return this.printSigantureFields; + } + + /** + * Print stamp annotations on pdf + * + * @param printStampAnnotation + */ + public void setPrintStampAnnotations(boolean printStampAnnotations) { + this.printStampAnnotations = printStampAnnotations; + } + + /** + * @return true if stamp annotations will be printed on pdf + */ + public boolean isPrintStampAnnotations() { + return this.printStampAnnotations; + } + + /** + * Print widget annotations on pdf + * + * @param printWidgetAnnotations + */ + public void setPrintWidgetAnnotations(boolean printWidgetAnnotations) { + this.printWidgetAnnotations = printWidgetAnnotations; + } + + /** + * @return true if widget annotations will be printed on pdf + */ + public boolean isPrintWidgetAnnotations() { + return this.printWidgetAnnotations; + } + + /** + * Print freetext annotations on pdf + * + * @param printFreetextAnnotations + */ + public void setPrintFreetextAnnotations(boolean printFreetextAnnotations) { + this.printFreetextAnnotations = printFreetextAnnotations; + } + + /** + * @return true if freetext annotations will be printed on pdf + */ + public boolean isPrintFreetextAnnotations() { + return this.printFreetextAnnotations; + } + + /** + * Print link annotations on pdf + * + * @param printLinkAnnotations + */ + public void setPrintLinkAnnotations(boolean printLinkAnnotations) { + this.printLinkAnnotations = printLinkAnnotations; + } + + /** + * @return true if link annotations will be printed on pdf + */ + public boolean isPrintLinkAnnotations() { + return this.printLinkAnnotations; + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/HexDump.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/HexDump.java new file mode 100644 index 0000000000..def6ce790a --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/HexDump.java @@ -0,0 +1,84 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer; + +import java.io.IOException; +import java.io.RandomAccessFile; + +public class HexDump { + + public static void printData(byte[] data) { + char[] parts = new char[17]; + int partsloc = 0; + for (int i = 0; i < data.length; i++) { + int d = (data[i]) & 0xff; + if (d == 0) { + parts[partsloc++] = '.'; + } else if (d < 32 || d >= 127) { + parts[partsloc++] = '?'; + } else { + parts[partsloc++] = (char) d; + } + if (i % 16 == 0) { + int start = Integer.toHexString(data.length).length(); + int end = Integer.toHexString(i).length(); + + for (int j = start; j > end; j--) { + System.out.print("0"); + } + System.out.print(Integer.toHexString(i) + ": "); + } + if (d < 16) { + System.out.print("0" + Integer.toHexString(d)); + } else { + System.out.print(Integer.toHexString(d)); + } + if ((i & 15) == 15 || i == data.length - 1) { + System.out.println(" " + new String(parts)); + partsloc = 0; + } else if ((i & 7) == 7) { + System.out.print(" "); + parts[partsloc++] = ' '; + } else if ((i & 1) == 1) { + System.out.print(" "); + } + } + System.out.println(); + } + + public static void main(String args[]) { + if (args.length != 1) { + System.out.println("Usage: "); + System.out.println(" HexDump "); + System.exit(-1); + } + + try { + RandomAccessFile raf = new RandomAccessFile(args[0], "r"); + + int size = (int) raf.length(); + byte[] data = new byte[size]; + + raf.readFully(data); + printData(data); + } catch (IOException ioe) { + BaseWatchable.getErrorHandler().publishException(ioe); + } + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/Identity8BitCharsetEncoder.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/Identity8BitCharsetEncoder.java new file mode 100644 index 0000000000..5a039c8264 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/Identity8BitCharsetEncoder.java @@ -0,0 +1,61 @@ +/* + * Copyright 2008 Pirion Systems Pty Ltd, 139 Warry St, + * Fortitude Valley, Queensland, Australia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer; + +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.nio.CharBuffer; +import java.nio.ByteBuffer; + +/** + * A {@link CharsetEncoder} that attempts to write out the lower 8 bits + * of any character. Characters >= 256 in value are regarded + * as unmappable. + * + * @author Luke Kirby + */ +public class Identity8BitCharsetEncoder extends CharsetEncoder { + + public Identity8BitCharsetEncoder() { + super(null, 1, 1); + } + + @Override + protected CoderResult encodeLoop(CharBuffer in, ByteBuffer out) { + while (in.remaining() > 0) { + if (out.remaining() < 1) { + return CoderResult.OVERFLOW; + } + final char c = in.get(); + if (c >= 0 && c < 256) { + out.put((byte) c); + } else { + return CoderResult.unmappableForLength(1); + } + } + return CoderResult.UNDERFLOW; + } + + @Override + public boolean isLegalReplacement(byte[] repl) { + // avoid referencing the non-existent character set + return true; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/ImageInfo.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/ImageInfo.java new file mode 100644 index 0000000000..60d04f66a3 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/ImageInfo.java @@ -0,0 +1,74 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer; + +import java.awt.geom.Rectangle2D; +import java.awt.Color; + +public class ImageInfo { + + int width; + int height; + Rectangle2D clip; + Color bgColor; + + public ImageInfo(int width, int height, Rectangle2D clip) { + this(width, height, clip, Color.WHITE); + } + + public ImageInfo(int width, int height, Rectangle2D clip, Color bgColor) { + this.width = width; + this.height = height; + this.clip = clip; + this.bgColor = bgColor; + } + + // a hashcode that uses width, height and clip to generate its number + @Override + public int hashCode() { + int code = (this.width ^ this.height << 16); + + if (this.clip != null) { + code ^= ((int) this.clip.getWidth() | (int) this.clip.getHeight()) << 8; + code ^= ((int) this.clip.getMinX() | (int) this.clip.getMinY()); + } + + return code; + } + + // an equals method that compares values + @Override + public boolean equals(Object o) { + if (!(o instanceof ImageInfo)) { + return false; + } + + ImageInfo ii = (ImageInfo) o; + + if (this.width != ii.width || this.height != ii.height) { + return false; + } else if (this.clip != null && ii.clip != null) { + return this.clip.equals(ii.clip); + } else if (this.clip == null && ii.clip == null) { + return true; + } else { + return false; + } + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/NameTree.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/NameTree.java new file mode 100644 index 0000000000..2ca215f6c5 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/NameTree.java @@ -0,0 +1,131 @@ +/* + * $Id: NameTree.java,v 1.3 2009/01/16 16:26:09 tomoke Exp $ + * + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer; + +import java.io.IOException; + +/** + * A PDF name tree consists of three kinds of nodes: + *
    + *
  • The root node contains only a kids entry, pointing to many + * other objects + *
  • An intermediate node contains the limits of all the children in + * its subtree, and a kids entry for each child + *
  • A leaf node contains a set of name-to-object mappings in a dictionary, + * as well as the limits of the data contained in that child. + *
+ * A PDF name tree is sorted in accordance with the String.compareTo() method. + */ +public class NameTree { + + /** the root object */ + private PDFObject root; + + /** Creates a new instance of NameTree */ + public NameTree(PDFObject root) { + this.root = root; + } + + /** + * Find the PDF object corresponding to the given String in a name tree + * + * @param key the key we are looking for in the name tree + * @return the object associated with str, if found, or null if not + */ + public PDFObject find(String key) throws IOException { + return find(root, key); + } + + /** + * Recursively walk the name tree looking for a given value + */ + private PDFObject find(PDFObject root, String key) + throws IOException { + // first, look for a Names entry, meaning this is a leaf + PDFObject names = root.getDictRef("Names"); + if (names != null) { + return findInArray(names.getArray(), key); + } + + // no names given, look for kids + PDFObject kidsObj = root.getDictRef("Kids"); + if (kidsObj != null) { + PDFObject[] kids = kidsObj.getArray(); + + for (int i = 0; i < kids.length; i++) { + // find the limits of this kid + PDFObject limitsObj = kids[i].getDictRef("Limits"); + if (limitsObj != null) { + String lowerLimit = limitsObj.getAt(0).getStringValue(); + String upperLimit = limitsObj.getAt(1).getStringValue(); + + // are we in range? + if ((key.compareTo(lowerLimit) >= 0) && + (key.compareTo(upperLimit) <= 0)) { + + // we are, so find in this child + return find(kids[i], key); + } + } + } + } + + // no luck + return null; + } + + /** + * Find an object in a (key,value) array. Do this by splitting in half + * repeatedly. + */ + private PDFObject findInArray(PDFObject[] array, String key) + throws IOException { + int start = 0; + int end = array.length / 2; + + while (end >= start && start >= 0 && end < array.length) { + // find the key at the midpoint + int pos = start + ((end - start) / 2); + String posKey = array[pos * 2].getStringValue(); + + // compare the key to the key we are looking for + int comp = key.compareTo(posKey); + if (comp == 0) { + // they match. Return the value + int tmp = (pos * 2) + 1; + if(array.length>tmp){ + return array[tmp]; + }else { + return null; + } + } else if (comp > 0) { + // too big, search the top half of the tree + start = pos + 1; + } else if (comp < 0) { + // too small, search the bottom half of the tree + end = pos - 1; + } + } + + // not found + return null; + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/OutlineNode.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/OutlineNode.java new file mode 100644 index 0000000000..ba70d1cf25 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/OutlineNode.java @@ -0,0 +1,60 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer; + +import javax.swing.tree.DefaultMutableTreeNode; + +import com.github.librepdf.pdfrenderer.action.PDFAction; + +public class OutlineNode extends DefaultMutableTreeNode { + // the name of this node + + private String title; + + /** + * Create a new outline node + * + * @param title the node's visible name in the tree + */ + public OutlineNode(String title) { + this.title = title; + } + + /** + * Get the PDF action associated with this node + */ + public PDFAction getAction() { + return (PDFAction) getUserObject(); + } + + /** + * Set the PDF action associated with this node + */ + public void setAction(PDFAction action) { + setUserObject(action); + } + + /** + * Return the node's visible name in the tree + */ + @Override + public String toString() { + return this.title; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFCmd.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFCmd.java new file mode 100644 index 0000000000..97de13f433 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFCmd.java @@ -0,0 +1,59 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer; + +import java.awt.geom.Rectangle2D; + +/** + * The abstract superclass of all drawing commands for a PDFPage. + * @author Mike Wessler + */ +public abstract class PDFCmd { + + /** + * mark the page or change the graphics state + * @param state the current graphics state; may be modified during + * execution. + * @return the region of the page made dirty by executing this command + * or null if no region was touched. Note this value should be + * in the coordinates of the image touched, not the page. + */ + public abstract Rectangle2D execute(PDFRenderer state); + + /** + * a human readable representation of this command + */ + @Override + public String toString() { + String name = getClass().getName(); + int lastDot = name.lastIndexOf('.'); + if (lastDot >= 0) { + return name.substring(lastDot + 1); + } else { + return name; + } + } + + /** + * the details of this command + */ + public String getDetails() { + return super.toString(); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFDebugger.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFDebugger.java new file mode 100644 index 0000000000..9c3b3d8838 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFDebugger.java @@ -0,0 +1,136 @@ +package com.github.librepdf.pdfrenderer; + +import java.awt.Rectangle; +import java.awt.geom.GeneralPath; +import java.awt.geom.Point2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.HashMap; + +import javax.imageio.ImageIO; + +public class PDFDebugger { + public final static String DEBUG_DCTDECODE_DATA = "debugdctdecode"; + public static final boolean DEBUG_TEXT = false; + public static final boolean DEBUG_IMAGES = false; + public static final boolean DEBUG_OPERATORS = false; + public static final boolean DEBUG_PATH = false; + public static final int DEBUG_STOP_AT_INDEX = 0; + public static final boolean DISABLE_TEXT = false; + public static final boolean DISABLE_IMAGES = false; + public static final boolean DISABLE_PATH_STROKE = false; + public static final boolean DISABLE_PATH_FILL = false; + public static final boolean DISABLE_PATH_STROKE_FILL = false; + public static final boolean DISABLE_CLIP = false; + public static final boolean DISABLE_FORMS = false; + public static final boolean DISABLE_SHADER = false; + public static final boolean SHOW_TEXT_REGIONS = false; + public static final boolean SHOW_TEXT_ANCHOR = false; + public static final boolean DISABLE_THUMBNAILS = false; + public static final long DRAW_DELAY = 0; + + public static int debuglevel = 4000; + + @SuppressWarnings("serial") + public static class DebugStopException extends Exception { + // nothing to do + } + + public static void debugImage(BufferedImage image, String name) { + if (PDFDebugger.DEBUG_IMAGES) { + if(image == null) { + return; + } + try { + // retrieve image + File outputfile = new File("D:/tmp/PDFimages/" + name + ".png"); + ImageIO.write(image, "png", outputfile); + } catch (IOException e) { + BaseWatchable.getErrorHandler().publishException(e); + } + } + } + + // TODO: add debug level and print it? + public static void debug(String msg, int level) { + if (level > debuglevel) { + System.out.println(escape(msg)); + } + } + + // TODO: add debug level and print it? + public static void debug(String msg) { + debug(msg, debuglevel); + } + + public static String escape(String msg) { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < msg.length(); i++) { + char c = msg.charAt(i); + if (c != '\n' && (c < 32 || c >= 127)) { + c = '?'; + } + sb.append(c); + } + return sb.toString(); + } + + public static void setDebugLevel(int level) { + debuglevel = level; + } + + + public static String dumpStream(byte[] stream) { + return PDFDebugger.escape(new String(stream).replace('\r', '\n')); + } + + public static void logPath(GeneralPath path, String operation) { + if (PDFDebugger.DEBUG_PATH){ + if (operation != null) { + System.out.println("Operation: " + operation + "; "); + } + System.out.println("Current path: "); + Rectangle b = path.getBounds(); + if (b != null) + System.out.println(" Bounds [x=" + b.x + ",y=" + b.y + ",width=" + b.width + ",height=" + b.height + "]"); + Point2D p = path.getCurrentPoint(); + if (p != null) + System.out.println(" Point [x=" + p.getX() + ",y=" + p.getY() + "]"); + } + } + + /** + * take a byte array and write a temporary file with it's data. + * This is intended to capture data for analysis, like after decoders. + * + * @param ary + * @param name + */ + public static void emitDataFile(byte[] ary, String name) { + FileOutputStream ostr; + try { + File file = File.createTempFile("DateFile", name); + ostr = new FileOutputStream(file); + PDFDebugger.debug("Write: " + file.getPath()); + ostr.write(ary); + ostr.close(); + } catch (IOException ex) { + // ignore + } + } + + public static void dump(PDFObject obj) throws IOException { + PDFDebugger.debug("dumping PDF object: " + obj); + if (obj == null) { + return; + } + HashMap dict = obj.getDictionary(); + PDFDebugger.debug(" dict = " + dict); + for (Object key : dict.keySet()) { + PDFDebugger.debug("key = " + key + " value = " + dict.get(key)); + } + } + +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFDestination.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFDestination.java new file mode 100644 index 0000000000..a3804d8508 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFDestination.java @@ -0,0 +1,299 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer; + +import java.io.IOException; + +/** + * Represents a destination in a PDF file. Destinations take 3 forms: + *
    + *
  • An explicit destination, which contains a reference to a page as well as + * some stuff about how to fit it into the window. + *
  • A named destination, which uses the PDF file's Dests entry in the + * document catalog to map a name to an explicit destination + *
  • A string destintation, which uses the PDF file's Dests entry. in the name + * directory to map a string to an explicit destination. + *
+ * + * All three of these cases are handled by the getDestination() method. + */ +public class PDFDestination { + + /** The known types of destination */ + public static final int XYZ = 0; + public static final int FIT = 1; + public static final int FITH = 2; + public static final int FITV = 3; + public static final int FITR = 4; + public static final int FITB = 5; + public static final int FITBH = 6; + public static final int FITBV = 7; + /** the type of this destination (from the list above) */ + private int type; + /** the page we refer to */ + private PDFObject pageObj; + /** the left coordinate of the fit area, if applicable */ + private float left; + /** the right coordinate of the fit area, if applicable */ + private float right; + /** the top coordinate of the fit area, if applicable */ + private float top; + /** the bottom coordinate of the fit area, if applicable */ + private float bottom; + /** the zoom, if applicable */ + private float zoom; + + /** + * Creates a new instance of PDFDestination + * + * @param pageObj + * the page object this destination refers to + * @param type + * the type of page this object refers to + */ + protected PDFDestination(PDFObject pageObj, int type) { + this.pageObj = pageObj; + this.type = type; + } + + /** + * Get a destination from either an array (explicit destination), a name + * (named destination) or a string (name tree destination). + * + * @param obj + * the PDFObject representing this destination + * @param root + * the root of the PDF object tree + */ + public static PDFDestination getDestination(PDFObject obj, PDFObject root) throws IOException { + // resolve string and name issues + if (obj.getType() == PDFObject.NAME) { + obj = getDestFromName(obj, root); + } else if (obj.getType() == PDFObject.STRING) { + obj = getDestFromString(obj, root); + } + + // make sure we have the right kind of object + if (obj == null || obj.getType() != PDFObject.ARRAY) { + throw new PDFParseException("Can't create destination from: " + obj); + } + + // the array is in the form [page type args ... ] + PDFObject[] destArray = obj.getArray(); + + // create the destination based on the type + PDFDestination dest = null; + String type = destArray[1].getStringValue(); + if (type.equals("XYZ")) { + dest = new PDFDestination(destArray[0], XYZ); + } else if (type.equals("Fit")) { + dest = new PDFDestination(destArray[0], FIT); + } else if (type.equals("FitH")) { + dest = new PDFDestination(destArray[0], FITH); + } else if (type.equals("FitV")) { + dest = new PDFDestination(destArray[0], FITV); + } else if (type.equals("FitR")) { + dest = new PDFDestination(destArray[0], FITR); + } else if (type.equals("FitB")) { + dest = new PDFDestination(destArray[0], FITB); + } else if (type.equals("FitBH")) { + dest = new PDFDestination(destArray[0], FITBH); + } else if (type.equals("FitBV")) { + dest = new PDFDestination(destArray[0], FITBV); + } else { + throw new PDFParseException("Unknown destination type: " + type); + } + + // now fill in the arguments based on the type + switch (dest.getType()) { + case XYZ: + dest.setLeft(destArray[2].getFloatValue()); + dest.setTop(destArray[3].getFloatValue()); + dest.setZoom(destArray[4].getFloatValue()); + break; + case FITH: + if (destArray.length > 2) { + dest.setTop(destArray[2].getFloatValue()); + } else { + dest.setTop(0.0F); + } + break; + case FITV: + if (destArray.length > 2) { + dest.setTop(destArray[2].getFloatValue()); + } else { + dest.setTop(0.0F); + } + break; + case FITR: + dest.setLeft(destArray[2].getFloatValue()); + dest.setBottom(destArray[3].getFloatValue()); + dest.setRight(destArray[4].getFloatValue()); + dest.setTop(destArray[5].getFloatValue()); + break; + case FITBH: + if (destArray.length > 2) { + dest.setTop(destArray[2].getFloatValue()); + } else { + dest.setTop(0.0F); + } + break; + case FITBV: + if (destArray.length > 2) { + dest.setTop(destArray[2].getFloatValue()); + } else { + dest.setTop(0.0F); + } + break; + } + + return dest; + } + + /** + * Get the type of this destination + */ + public int getType() { + return this.type; + } + + /** + * Get the PDF Page object associated with this destination + */ + public PDFObject getPage() { + return this.pageObj; + } + + /** + * Get the left coordinate value + */ + public float getLeft() { + return this.left; + } + + /** + * Set the left coordinate value + */ + public void setLeft(float left) { + this.left = left; + } + + /** + * Get the right coordinate value + */ + public float getRight() { + return this.right; + } + + /** + * Set the right coordinate value + */ + public void setRight(float right) { + this.right = right; + } + + /** + * Get the top coordinate value + */ + public float getTop() { + return this.top; + } + + /** + * Set the top coordinate value + */ + public void setTop(float top) { + this.top = top; + } + + /** + * Get the bottom coordinate value + */ + public float getBottom() { + return this.bottom; + } + + /** + * Set the bottom coordinate value + */ + public void setBottom(float bottom) { + this.bottom = bottom; + } + + /** + * Get the zoom value + */ + public float getZoom() { + return this.zoom; + } + + /** + * Set the zoom value + */ + public void setZoom(float zoom) { + this.zoom = zoom; + } + + /** + * Get a destination, given a name. This means the destination is in the + * root node's dests dictionary. + */ + private static PDFObject getDestFromName(PDFObject name, PDFObject root) throws IOException { + // find the dests object in the root node + PDFObject dests = root.getDictRef("Dests"); + if (dests != null) { + // find this name in the dests dictionary + return dests.getDictRef(name.getStringValue()); + } + + // not found + return null; + } + + /** + * Get a destination, given a string. This means the destination is in the + * root node's names dictionary. + */ + private static PDFObject getDestFromString(PDFObject str, PDFObject root) throws IOException { + // find the names object in the root node + PDFObject names = root.getDictRef("Names"); + if (names != null) { + // find the dests entry in the names dictionary + PDFObject dests = names.getDictRef("Dests"); + if (dests != null) { + // create a name tree object + NameTree tree = new NameTree(dests); + + // find the value we're looking for + PDFObject obj = tree.find(str.getStringValue()); + + // if we get back a dictionary, look for the /D value + if (obj != null && obj.getType() == PDFObject.DICTIONARY) { + obj = obj.getDictRef("D"); + } + + // found it + return obj; + } + } + + // not found + return null; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFDocCharsetEncoder.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFDocCharsetEncoder.java new file mode 100644 index 0000000000..f067186e0e --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFDocCharsetEncoder.java @@ -0,0 +1,106 @@ +/* Copyright 2008 Pirion Systems Pty Ltd, 139 Warry St, + * Fortitude Valley, Queensland, Australia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.util.HashMap; +import java.util.Map; + +/** + * Encodes into a PDFDocEncoding representation. Note that only 256 characters + * (if that) are represented in the PDFDocEncoding, so users should be + * prepared to deal with unmappable character exceptions. + * + * @see "PDF Reference version 1.7, Appendix D" + * + * @author Luke Kirby + */ +public class PDFDocCharsetEncoder extends CharsetEncoder { + + /** + * Identify whether a particular character preserves the same byte value + * upon encoding in PDFDocEncoding + * @param ch the character + * @return whether the character is identity encoded + */ + public static boolean isIdentityEncoding(char ch) { + return ch >= 0 && ch <= 255 && IDENT_PDF_DOC_ENCODING_MAP[ch]; + + } + + /** + * For each character that exists in PDFDocEncoding, identifies whether + * the byte value in UTF-16BE is the same as it is in PDFDocEncoding + */ + final static boolean[] IDENT_PDF_DOC_ENCODING_MAP = new boolean[256]; + + /** + * For non-identity encoded characters, maps from the character to + * the byte value in PDFDocEncoding. If an entry for a non-identity + * coded character is absent from this map, that character is unmappable + * in the PDFDocEncoding. + */ + final static Map EXTENDED_TO_PDF_DOC_ENCODING_MAP = + new HashMap(); + static + { + for (byte i = 0; i < PDFStringUtil.PDF_DOC_ENCODING_MAP.length; ++i) { + final char c = PDFStringUtil.PDF_DOC_ENCODING_MAP[i]; + final boolean identical = (c == i); + IDENT_PDF_DOC_ENCODING_MAP[i] = identical; + if (!identical) { + EXTENDED_TO_PDF_DOC_ENCODING_MAP.put(c, i); + } + } + } + + public PDFDocCharsetEncoder() { + super(null, 1, 1); + } + + @Override + protected CoderResult encodeLoop(CharBuffer in, ByteBuffer out) { + while (in.remaining() > 0) { + if (out.remaining() < 1) { + return CoderResult.OVERFLOW; + } + final char c = in.get(); + if (c >= 0 && c < 256 && IDENT_PDF_DOC_ENCODING_MAP[c]) { + out.put((byte) c); + } else { + final Byte mapped = EXTENDED_TO_PDF_DOC_ENCODING_MAP.get(c); + if (mapped != null) { + out.put(mapped); + } else { + return CoderResult.unmappableForLength(1); + } + } + } + return CoderResult.UNDERFLOW; + } + + @Override + public boolean isLegalReplacement(byte[] repl) { + // avoid referencing the non-existent character set + return true; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFErrorHandler.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFErrorHandler.java new file mode 100644 index 0000000000..833d6fe56f --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFErrorHandler.java @@ -0,0 +1,14 @@ +package com.github.librepdf.pdfrenderer; + +/** + * Simple class to handle exceptions - as default we just print the stack trace + * but it's possible to inject another behaviour + * @author xond + * + */ +public class PDFErrorHandler { + + public void publishException(Throwable e){ + e.printStackTrace(); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFFile.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFFile.java new file mode 100644 index 0000000000..f77c834996 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFFile.java @@ -0,0 +1,1875 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer; + +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +import com.github.librepdf.pdfrenderer.action.GoToAction; +import com.github.librepdf.pdfrenderer.action.PDFAction; +import com.github.librepdf.pdfrenderer.annotation.PDFAnnotation; +import com.github.librepdf.pdfrenderer.decrypt.EncryptionUnsupportedByPlatformException; +import com.github.librepdf.pdfrenderer.decrypt.EncryptionUnsupportedByProductException; +import com.github.librepdf.pdfrenderer.decrypt.IdentityDecrypter; +import com.github.librepdf.pdfrenderer.decrypt.PDFAuthenticationFailureException; +import com.github.librepdf.pdfrenderer.decrypt.PDFDecrypter; +import com.github.librepdf.pdfrenderer.decrypt.PDFDecrypterFactory; +import com.github.librepdf.pdfrenderer.decrypt.PDFPassword; +import com.github.librepdf.pdfrenderer.decrypt.UnsupportedEncryptionException; + +/** + * An encapsulation of a .pdf file. The methods of this class + * can parse the contents of a PDF file, but those methods are + * hidden. Instead, the public methods of this class allow + * access to the pages in the PDF file. Typically, you create + * a new PDFFile, ask it for the number of pages, and then + * request one or more PDFPages. + * @author Mike Wessler + */ +public class PDFFile { + + public final static int NUL_CHAR = 0; + public final static int FF_CHAR = 12; + + private String versionString = "1.1"; + private int majorVersion = 1; + private int minorVersion = 1; + /** the end of line character */ + /** the comment text to begin the file to determine it's version */ + private final static String VERSION_COMMENT = "%PDF-"; + /** + * A ByteBuffer containing the file data + */ + ByteBuffer buf; + /** + * the cross reference table mapping object numbers to locations + * in the PDF file + */ + PDFXref[] objIdx; + /** the root PDFObject, as specified in the PDF file */ + PDFObject root = null; + /** the Encrypt PDFObject, from the trailer */ + PDFObject encrypt = null; + + /** The Info PDFPbject, from the trailer, for simple metadata */ + PDFObject info = null; + + /** a mapping of page numbers to parsed PDF commands */ + Cache cache; + /** + * whether the file is printable or not (trailer -> Encrypt -> P & 0x4) + */ + private boolean printable = true; + /** + * whether the file is saveable or not (trailer -> Encrypt -> P & 0x10) + */ + private boolean saveable = true; + + /** + * The default decrypter for streams and strings. By default, no + * encryption is expected, and thus the IdentityDecrypter is used. + */ + private PDFDecrypter defaultDecrypter = IdentityDecrypter.getInstance(); + + /** + * get a PDFFile from a .pdf file. The file must me a random access file + * at the moment. It should really be a file mapping from the nio package. + *

+ * Use the getPage(...) methods to get a page from the PDF file. + * @param buf the RandomAccessFile containing the PDF. + * @throws IOException if there's a problem reading from the buffer + * @throws PDFParseException if the document appears to be malformed, or + * its features are unsupported. If the file is encrypted in a manner that + * the product or platform does not support then the exception's {@link + * PDFParseException#getCause() cause} will be an instance of {@link + * UnsupportedEncryptionException}. + * @throws PDFAuthenticationFailureException if the file is password + * protected and requires a password + */ + public PDFFile(ByteBuffer buf) throws IOException { + this(buf, null); + } + + public PDFFile(ByteBuffer buf, boolean doNotParse) throws IOException { + this.buf = buf; + } + + /** + * get a PDFFile from a .pdf file. The file must me a random access file + * at the moment. It should really be a file mapping from the nio package. + *

+ * Use the getPage(...) methods to get a page from the PDF file. + * @param buf the RandomAccessFile containing the PDF. + * @param password the user or owner password + * @throws IOException if there's a problem reading from the buffer + * @throws PDFParseException if the document appears to be malformed, or + * its features are unsupported. If the file is encrypted in a manner that + * the product or platform does not support then the exception's {@link + * PDFParseException#getCause() cause} will be an instance of {@link + * UnsupportedEncryptionException}. + * @throws PDFAuthenticationFailureException if the file is password + * protected and the supplied password does not decrypt the document + */ + public PDFFile(ByteBuffer buf, PDFPassword password) throws IOException { + this.buf = buf; + + this.cache = new Cache(); + + parseFile(password); + } + + /** + * Gets whether the owner of the file has given permission to print + * the file. + * @return true if it is okay to print the file + */ + public boolean isPrintable() { + return this.printable; + } + + /** + * Gets whether the owner of the file has given permission to save + * a copy of the file. + * @return true if it is okay to save the file + */ + public boolean isSaveable() { + return this.saveable; + } + + /** + * get the root PDFObject of this PDFFile. You generally shouldn't need + * this, but we've left it open in case you want to go spelunking. + */ + public PDFObject getRoot() { + return this.root; + } + + /** + * return the number of pages in this PDFFile. The pages will be + * numbered from 1 to getNumPages(), inclusive. + */ + public int getNumPages() { + try { + return this.root.getDictRef("Pages").getDictRef("Count").getIntValue(); + } catch (Exception ioe) { + return 0; + } + } + + /** + * Get metadata (e.g., Author, Title, Creator) from the Info dictionary + * as a string. + * @param name the name of the metadata key (e.g., Author) + * @return the info + * @throws IOException if the metadata cannot be read + */ + public String getStringMetadata(String name) + throws IOException { + if (this.info != null) { + final PDFObject meta = this.info.getDictRef(name); + return meta != null ? meta.getTextStringValue() : null; + } else { + return null; + } + } + + /** + * Get the keys into the Info metadata, for use with + * {@link #getStringMetadata(String)} + * @return the keys present into the Info dictionary + * @throws IOException if the keys cannot be read + */ + public Iterator getMetadataKeys() + throws IOException { + if (this.info != null) { + return this.info.getDictKeys(); + } else { + return Collections.emptyList().iterator(); + } + } + + + /** + * Used internally to track down PDFObject references. You should never + * need to call this. + *

+ * Since this is the only public method for tracking down PDF objects, + * it is synchronized. This means that the PDFFile can only hunt down + * one object at a time, preventing the file's location from getting + * messed around. + *

+ * This call stores the current buffer position before any changes are made + * and restores it afterwards, so callers need not know that the position + * has changed. + * + */ + public synchronized PDFObject dereference(PDFXref ref, PDFDecrypter decrypter) + throws IOException { + int id = ref.getID(); + + // make sure the id is valid and has been read + if (id >= this.objIdx.length || this.objIdx[id] == null) { + return PDFObject.nullObj; + } + + // check to see if this is already dereferenced + PDFObject obj = this.objIdx[id].getObject(); + if (obj != null) { + return obj; + } + + // store the current position in the buffer + int startPos = this.buf.position(); + + boolean compressed = this.objIdx[id].getCompressed(); + if (!compressed) { + int loc = this.objIdx[id].getFilePos(); + if (loc < 0) { + return PDFObject.nullObj; + } + + // move to where this object is + this.buf.position(loc); + + // read the object and cache the reference + obj= readObject(ref.getID(), ref.getGeneration(), decrypter); + } + else { // compressed + int compId = this.objIdx[id].getID(); + int idx = this.objIdx[id].getIndex(); + if (idx < 0) + return PDFObject.nullObj; + PDFXref compRef = new PDFXref(compId, 0); + PDFObject compObj = dereference(compRef, decrypter); + int first = compObj.getDictionary().get("First").getIntValue(); + int length = compObj.getDictionary().get("Length").getIntValue(); + int n = compObj.getDictionary().get("N").getIntValue(); + if (idx >= n) + return PDFObject.nullObj; + ByteBuffer strm = compObj.getStreamBuffer(); + + ByteBuffer oldBuf = this.buf; + this.buf = strm; + // skip other nums + for (int i=0; i + * + * ISO 32000-1:2008 - Table 2 + * + * @param c the character to test + */ + public static boolean isDelimiter(int c) { + switch (c) { + case '(': // LEFT PARENTHESIS + case ')': // RIGHT PARENTHESIS + case '<': // LESS-THAN-SIGN + case '>': // GREATER-THAN-SIGN + case '[': // LEFT SQUARE BRACKET + case ']': // RIGHT SQUARE BRACKET + case '{': // LEFT CURLY BRACKET + case '}': // RIGHT CURLY BRACKET + case '/': // SOLIDUS + case '%': // PERCENT SIGN + return true; + default: + return false; + } + } + + /** + * return true if the character is neither a whitespace or a delimiter. + * + * @param c the character to test + * @return boolean + */ + public static boolean isRegularCharacter (int c) { + return !(isWhiteSpace(c) || isDelimiter(c)); + } + + /** + * read the next object from the file + * @param objNum the object number of the object containing the object + * being read; negative only if the object number is unavailable (e.g., if + * reading from the trailer, or reading at the top level, in which + * case we can expect to be reading an object description) + * @param objGen the object generation of the object containing the object + * being read; negative only if the objNum is unavailable + * @param decrypter the decrypter to use + */ + private PDFObject readObject( + int objNum, int objGen, PDFDecrypter decrypter) throws IOException { + return readObject(objNum, objGen, false, decrypter); + } + + /** + * read the next object with a special catch for numbers + * @param numscan if true, don't bother trying to see if a number is + * an object reference (used when already in the middle of testing for + * an object reference, and not otherwise) + * @param objNum the object number of the object containing the object + * being read; negative only if the object number is unavailable (e.g., if + * reading from the trailer, or reading at the top level, in which + * case we can expect to be reading an object description) + * @param objGen the object generation of the object containing the object + * being read; negative only if the objNum is unavailable + * @param decrypter the decrypter to use + */ + private PDFObject readObject( + int objNum, int objGen, + boolean numscan, PDFDecrypter decrypter) throws IOException { + // skip whitespace + int c; + PDFObject obj = null; + while (obj == null && this.buf.hasRemaining()) { + while (isWhiteSpace(c = this.buf.get())) { + if(!buf.hasRemaining()) { + break; + } + } + // check character for special punctuation: + if (c == '<') { + // could be start of , or start of <> + c = this.buf.get(); + if (c == '<') { + // it's a dictionary + obj= readDictionary(objNum, objGen, decrypter); + } else { + this.buf.position(this.buf.position() - 1); + obj= readHexString(objNum, objGen, decrypter); + } + } else if (c == '(') { + obj= readLiteralString(objNum, objGen, decrypter); + } else if (c == '[') { + // it's an array + obj= readArray(objNum, objGen, decrypter); + } else if (c == '/') { + // it's a name + obj = readName(); + } else if (c == '%') { + // it's a comment + readLine(); + } else if ((c >= '0' && c <= '9') || c == '-' || c == '+' || c == '.') { + // it's a number + obj = readNumber((char) c); + if (!numscan) { + // It could be the start of a reference. + // Check to see if there's another number, then "R". + // + // We can't use mark/reset, since this could be called + // from dereference, which already is using a mark + int startPos = this.buf.position(); + + PDFObject testnum= readObject(-1, -1, true, decrypter); + if (testnum != null && + testnum.getType() == PDFObject.NUMBER) { + PDFObject testR= readObject(-1, -1, true, decrypter); + if (testR != null && + testR.getType() == PDFObject.KEYWORD && + testR.getStringValue().equals("R")) { + // yup. it's a reference. + PDFXref xref = new PDFXref(obj.getIntValue(), + testnum.getIntValue()); + // Create a placeholder that will be dereferenced + // as needed + obj = new PDFObject(this, xref); + } else if (testR != null && + testR.getType() == PDFObject.KEYWORD && + testR.getStringValue().equals("obj")) { + // it's an object description + obj= readObjectDescription( + obj.getIntValue(), + testnum.getIntValue(), + decrypter); + } else { + this.buf.position(startPos); + } + } else { + this.buf.position(startPos); + } + } + } else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { + // it's a keyword + obj = readKeyword((char) c); + } else { + // it's probably a closing character. + // throwback + this.buf.position(this.buf.position() - 1); + break; + } + } + return obj; + } + + /** + * Get the next non-white space character + * @param buf the buffer to read from + * @return the next non-whitespace character + */ + private int nextNonWhitespaceChar(ByteBuffer buf) { + int c; + while (isWhiteSpace(c = buf.get())) { + // nothing + } + return c; + } + + /** + * Consume all sequential whitespace from the current buffer position, + * leaving the buffer positioned at non-whitespace + * @param buf the buffer to read from + */ + private void consumeWhitespace(ByteBuffer buf) { + nextNonWhitespaceChar(buf); + buf.position(buf.position() - 1); + } + + /** + * requires the next few characters (after whitespace) to match the + * argument. + * @param match the next few characters after any whitespace that + * must be in the file + * @return true if the next characters match; false otherwise. + */ + private boolean nextItemIs(String match) throws IOException { + // skip whitespace + int c = nextNonWhitespaceChar(buf); + for (int i = 0; i < match.length(); i++) { + if (i > 0) { + c = this.buf.get(); + } + if (c != match.charAt(i)) { + return false; + } + } + return true; + } + + /** + * process a version string, to determine the major and minor versions + * of the file. + * + * @param versionString + */ + private void processVersion(String versionString) { + try { + StringTokenizer tokens = new StringTokenizer(versionString, "."); + this.majorVersion = Integer.parseInt(tokens.nextToken()); + this.minorVersion = Integer.parseInt(tokens.nextToken()); + this.versionString = versionString; + } catch (Exception e) { + // ignore + } + } + + /** + * return the major version of the PDF header. + * + * @return int + */ + public int getMajorVersion() { + return this.majorVersion; + } + + /** + * return the minor version of the PDF header. + * + * @return int + */ + public int getMinorVersion() { + return this.minorVersion; + } + + /** + * return the version string from the PDF header. + * + * @return String + */ + public String getVersionString() { + return this.versionString; + } + + /** + * read an entire << dictionary >>. The initial + * << has already been read. + * @param objNum the object number of the object containing the dictionary + * being read; negative only if the object number is unavailable, which + * should only happen if we're reading a dictionary placed directly + * in the trailer + * @param objGen the object generation of the object containing the object + * being read; negative only if the objNum is unavailable + * @param decrypter the decrypter to use + * @return the Dictionary as a PDFObject. + */ + private PDFObject readDictionary( + int objNum, int objGen, PDFDecrypter decrypter) throws IOException { + HashMap hm = new HashMap(); + // we've already read the <<. Now get /Name obj pairs until >> + PDFObject name; + while ((name= readObject(objNum, objGen, decrypter))!=null) { + // make sure first item is a NAME + if (name.getType() != PDFObject.NAME) { + throw new PDFParseException("First item in dictionary must be a /Name. (Was " + name + ")"); + } + PDFObject value= readObject(objNum, objGen, decrypter); + if (value != null) { + hm.put(name.getStringValue(), value); + } + } + if (!nextItemIs(">>")) { + throw new PDFParseException("End of dictionary wasn't '>>'"); + } + return new PDFObject(this, PDFObject.DICTIONARY, hm); + } + + /** + * read a character, and return its value as if it were a hexidecimal + * digit. + * @return a number between 0 and 15 whose value matches the next + * hexidecimal character. Returns -1 if the next character isn't in + * [0-9a-fA-F] + */ + private int readHexDigit() throws IOException { + int a; + while (isWhiteSpace(a = this.buf.get())) { + } + switch (a) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + a -= '0'; + break; + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': + a -= 'a' - 10; + break; + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': + a -= 'A' - 10; + break; + default: + a = -1; + break; + } + return a; + } + + /** + * return the 8-bit value represented by the next two hex characters. + * If the next two characters don't represent a hex value, return -1 + * and reset the read head. If there is only one hex character, + * return its value as if there were an implicit 0 after it. + */ + private int readHexPair() throws IOException { + int first = readHexDigit(); + if (first < 0) { + this.buf.position(this.buf.position() - 1); + return -1; + } + int second = readHexDigit(); + if (second < 0) { + this.buf.position(this.buf.position() - 1); + return (first << 4); + } else { + return (first << 4) + second; + } + } + + /** + * read a < hex string >. The initial < has already been read. + * @param objNum the object number of the object containing the dictionary + * being read; negative only if the object number is unavailable, which + * should only happen if we're reading a string placed directly + * in the trailer + * @param objGen the object generation of the object containing the object + * being read; negative only if the objNum is unavailable + * @param decrypter the decrypter to use + */ + private PDFObject readHexString( + int objNum, int objGen, PDFDecrypter decrypter) throws IOException { + // we've already read the <. Now get the hex bytes until > + int val; + StringBuffer sb = new StringBuffer(); + while ((val = readHexPair()) >= 0) { + sb.append((char) val); + } + if (buf.hasRemaining() && this.buf.get() != '>') { + throw new PDFParseException("Bad character in Hex String"); + } + return new PDFObject(this, PDFObject.STRING, + decrypter.decryptString(objNum, objGen, sb.toString())); + } + + /** + *

read a ( character string ). The initial ( has already been read. + * Read until a *balanced* ) appears.

+ * + *

Section 3.2.3 of PDF Refernce version 1.7 defines the format of + * String objects. Regarding literal strings:

+ * + *
Within a literal string, the backslash (\) is used as an + * escape character for various purposes, such as to include newline + * characters, nonprinting ASCII characters, unbalanced parentheses, or + * the backslash character itself in the string. The character + * immediately following the backslash determines its precise + * interpretation (see Table 3.2). If the character following the + * backslash is not one of those shown in the table, the backslash + * is ignored.
+ * + * *

This only reads 8 bit basic character 'strings' so as to avoid a + * text string interpretation when one is not desired (e.g., for byte + * strings, as used by the decryption mechanism). For an interpretation of + * a string returned from this method, where the object type is defined + * as a 'text string' as per Section 3.8.1, Table 3.31 "PDF Data Types", + * {@link PDFStringUtil#asTextString} ()} or + * {@link PDFObject#getTextStringValue()} must be employed.

+ * + * @param objNum the object number of the object containing the dictionary + * being read; negative only if the object number is unavailable, which + * should only happen if we're reading a dictionary placed directly + * in the trailer + * @param objGen the object generation of the object containing the object + * being read; negative only if the objNum is unavailable + * @param decrypter the decrypter to use + */ + private PDFObject readLiteralString( + int objNum, int objGen, PDFDecrypter decrypter) throws IOException { + int c; + + // we've already read the (. now get the characters until a + // *balanced* ) appears. Translate \r \n \t \b \f \( \) \\ \ddd + // if a cr/lf follows a backslash, ignore the cr/lf + int parencount = 1; + StringBuffer sb = new StringBuffer(); + + while (buf.hasRemaining() && parencount > 0) { + c = this.buf.get() & 0xFF; + // process unescaped parenthesis + if (c == '(') { + parencount++; + } else if (c == ')') { + parencount--; + if (parencount == 0) { + c = -1; + break; + } + } else if (c == '\\') { + + // From the spec: + // Within a literal string, the backslash (\) is used as an + // escape character for various purposes, such as to include + // newline characters, nonprinting ASCII characters, + // unbalanced parentheses, or the backslash character itself + // in the string. The character immediately following the + // backslash determines its precise interpretation (see + // Table 3.2). If the character following the backslash is not + // one of those shown in the table, the backslash is ignored. + // + // summary of rules: + // + // \n \r \t \b \f 2-char sequences are used to represent their + // 1-char counterparts + // + // \( and \) are used to escape parenthesis + // + // \\ for a literal backslash + // + // \ddd (1-3 octal digits) for a character code + // + // \ is used to put formatting newlines into the + // file, but aren't actually part of the string; EOL may be + // CR, LF or CRLF + // + // any other sequence should see the backslash ignored + + // grab the next character to see what we're dealing with + c = this.buf.get() & 0xFF; + if (c >= '0' && c < '8') { + // \ddd form - one to three OCTAL digits + int count = 0; + int val = 0; + while (c >= '0' && c < '8' && count < 3) { + val = val * 8 + c - '0'; + c = this.buf.get() & 0xFF; + count++; + } + // we'll have read one character too many + this.buf.position(this.buf.position() - 1); + c = val; + } else if (c == 'n') { + c = '\n'; + } else if (c == 'r') { + c = '\r'; + } else if (c == 't') { + c = '\t'; + } else if (c == 'b') { + c = '\b'; + } else if (c == 'f') { + c = '\f'; + } else if (c == '\r') { + // escaped CR to be ignored; look for a following LF + c = this.buf.get() & 0xFF; + if (c != '\n') { + // not an LF, we'll consume this character on + // the next iteration + this.buf.position(this.buf.position() - 1); + } + c = -1; + } else if (c == '\n') { + // escaped LF to be ignored + c = -1; + } + // any other c should be used as is, as it's either + // one of ()\ in which case it should be used literally, + // or the backslash should just be ignored + } + if (c >= 0) { + sb.append((char) c); + } + } + return new PDFObject(this, PDFObject.STRING, + decrypter.decryptString(objNum, objGen, sb.toString())); + } + + /** + * Read a line of text. This follows the semantics of readLine() in + * DataInput -- it reads character by character until a '\n' is + * encountered. If a '\r' is encountered, it is discarded. + */ + private String readLine() { + StringBuffer sb = new StringBuffer(); + + while (this.buf.remaining() > 0) { + char c = (char) this.buf.get(); + + if (c == '\r') { + if (this.buf.remaining() > 0) { + char n = (char) this.buf.get(this.buf.position()); + if (n == '\n') { + this.buf.get(); + } + } + break; + } else if (c == '\n') { + break; + } + + sb.append(c); + } + + return sb.toString(); + } + + /** + * read an [ array ]. The initial [ has already been read. PDFObjects + * are read until ]. + * @param objNum the object number of the object containing the dictionary + * being read; negative only if the object number is unavailable, which + * should only happen if we're reading an array placed directly + * in the trailer + * @param objGen the object generation of the object containing the object + * being read; negative only if the objNum is unavailable + * @param decrypter the decrypter to use + */ + private PDFObject readArray( + int objNum, int objGen, PDFDecrypter decrypter) throws IOException { + // we've already read the [. Now read objects until ] + ArrayList ary = new ArrayList(); + PDFObject obj; + while((obj= readObject(objNum, objGen, decrypter))!=null) { + ary.add(obj); + } + if (this.buf.hasRemaining() && this.buf.get() != ']') { + throw new PDFParseException("Array should end with ']'"); + } + PDFObject[] objlist = new PDFObject[ary.size()]; + for (int i = 0; i < objlist.length; i++) { + objlist[i] = ary.get(i); + } + return new PDFObject(this, PDFObject.ARRAY, objlist); + } + + /** + * read a /name. The / has already been read. + */ + private PDFObject readName() throws IOException { + // we've already read the / that begins the name. + // all we have to check for is #hh hex notations. + StringBuffer sb = new StringBuffer(); + int c; + while (this.buf.hasRemaining() && isRegularCharacter(c = this.buf.get())) { + if (c < '!' && c > '~') { + break; // out-of-range, should have been hex + } + // H.3.2.4 indicates version 1.1 did not do hex escapes + if (c == '#' && (this.majorVersion >= 1 && this.minorVersion > 1)) { + int hex = readHexPair(); + if (hex >= 0) { + c = hex; + } else { + throw new PDFParseException("Bad #hex in /Name"); + } + } + sb.append((char) c); + } + this.buf.position(this.buf.position() - 1); + return new PDFObject(this, PDFObject.NAME, sb.toString()); + } + + /** + * read a number. The initial digit or . or - is passed in as the + * argument. + */ + private PDFObject readNumber(char start) throws IOException { + // we've read the first digit (it's passed in as the argument) + boolean neg = start == '-'; + boolean sawdot = start == '.'; + double dotmult = sawdot ? 0.1 : 1; + double value = (start >= '0' && start <= '9') ? start - '0' : 0; + while (true && this.buf.hasRemaining()) { + int c = this.buf.get(); + if (c == '.') { + if (sawdot) { + throw new PDFParseException("Can't have two '.' in a number"); + } + sawdot = true; + dotmult = 0.1; + } else if (c >= '0' && c <= '9') { + int val = c - '0'; + if (sawdot) { + value += val * dotmult; + dotmult *= 0.1; + } else { + value = value * 10 + val; + } + } else { + this.buf.position(this.buf.position() - 1); + break; + } + } + if (neg) { + value = -value; + } + return new PDFObject(this, PDFObject.NUMBER, Double.valueOf(value)); + } + + /** + * read a bare keyword. The initial character is passed in as the + * argument. + */ + private PDFObject readKeyword(char start) throws IOException { + // we've read the first character (it's passed in as the argument) + StringBuffer sb = new StringBuffer(String.valueOf(start)); + int c; + while (buf.hasRemaining() && isRegularCharacter(c = this.buf.get())) { + sb.append((char) c); + } + this.buf.position(this.buf.position() - 1); + return new PDFObject(this, PDFObject.KEYWORD, sb.toString()); + } + + /** + * read an entire PDFObject. The intro line, which looks something + * like "4 0 obj" has already been read. + * @param objNum the object number of the object being read, being + * the first number in the intro line (4 in "4 0 obj") + * @param objGen the object generation of the object being read, being + * the second number in the intro line (0 in "4 0 obj"). + * @param decrypter the decrypter to use + */ + private PDFObject readObjectDescription( + int objNum, int objGen, PDFDecrypter decrypter) throws IOException { + // we've already read the 4 0 obj bit. Next thing up is the object. + // object descriptions end with the keyword endobj + long debugpos = this.buf.position(); + PDFObject obj = readObject(objNum, objGen, decrypter); + // see if it's a dictionary. If so, this could be a stream. + PDFObject endkey = readObject(objNum, objGen, decrypter); + if (endkey.getType() != PDFObject.KEYWORD && endkey.getType() != PDFObject.STREAM) { + PDFDebugger.debug("WARNING: Expected 'stream' or 'endobj' but was " + endkey.getType() + " " + String.valueOf(endkey.getStringValue())); + } + if (obj.getType() == PDFObject.DICTIONARY && endkey.getStringValue() != null && endkey.getStringValue().equals("stream")) { + // skip until we see \n + readLine(); + ByteBuffer data = readStream(obj); + if (data == null) { + data = ByteBuffer.allocate(0); + } + obj.setStream(data); + endkey = readObject(objNum, objGen, decrypter); + } + // at this point, obj is the object, keyword should be "endobj" + String endcheck = endkey.getStringValue(); + if (endcheck == null || !endcheck.equals("endobj")) { + PDFDebugger.debug("WARNING: object at " + debugpos + " didn't end with 'endobj'"); + } + obj.setObjectId(objNum, objGen); + return obj; + } + + /** + * read the stream portion of a PDFObject. Calls decodeStream to + * un-filter the stream as necessary. + * + * @param dict the dictionary associated with this stream. + * @return a ByteBuffer with the encoded stream data + */ + private ByteBuffer readStream(PDFObject dict) throws IOException { + // pointer is at the start of a stream. read the stream and + // decode, based on the entries in the dictionary + PDFObject lengthObj = dict.getDictRef("Length"); + int length = -1; + if (lengthObj != null) { + length = lengthObj.getIntValue(); + } + if (length < 0) { + throw new PDFParseException("Unknown length for stream"); + } + + // slice the data + int start = this.buf.position(); + ByteBuffer streamBuf = this.buf.slice(); + streamBuf.limit(length); + + // move the current position to the end of the data + this.buf.position(this.buf.position() + length); + int ending = this.buf.position(); + + if (!nextItemIs("endstream")) { + PDFDebugger.debug("read " + length + " chars from " + start + " to " + ending); + throw new PDFParseException("Stream ended inappropriately"); + } + + return streamBuf; + // now decode stream + // return PDFDecoder.decodeStream(dict, streamBuf); + } + + /** + * read the cross reference table from a PDF file. When this method + * is called, the file pointer must point to the start of the word + * "xref" in the file. Reads the xref table and the trailer dictionary. + * If dictionary has a /Prev entry, move file pointer + * and read new trailer + * @param password + */ + private void readTrailer(PDFPassword password) + throws + IOException, + PDFAuthenticationFailureException, + EncryptionUnsupportedByProductException, + EncryptionUnsupportedByPlatformException { + // the table of xrefs + this.objIdx = new PDFXref[50]; + + int pos = this.buf.position(); + + PDFDecrypter newDefaultDecrypter = null; + + // read a bunch of nested trailer tables + while (true) { + // make sure we are looking at an xref table + if (!nextItemIs("xref")) { + this.buf.position(pos); + readTrailer15(password); + return; +// throw new PDFParseException("Expected 'xref' at start of table"); + } + + // read a bunch of linked tabled + while (true) { + // read until the word "trailer" + PDFObject obj=readObject(-1, -1, IdentityDecrypter.getInstance()); + if (obj.getType() == PDFObject.KEYWORD && + obj.getStringValue().equals("trailer")) { + break; + } + + // read the starting position of the reference + if (obj.getType() != PDFObject.NUMBER) { + throw new PDFParseException("Expected number for first xref entry"); + } + int refstart = obj.getIntValue(); + + // read the size of the reference table + obj = readObject(-1, -1, IdentityDecrypter.getInstance()); + if (obj.getType() != PDFObject.NUMBER) { + throw new PDFParseException("Expected number for length of xref table"); + } + int reflen = obj.getIntValue(); + + // skip a line + readLine(); + + if (refstart == 1) {// Check and try to fix incorrect Object Number Start + int startPos = this.buf.position(); + try { + byte[] refline = new byte[20]; + this.buf.get(refline); + if (refline[17] == 'f') {// free + PDFXref objIndex = new PDFXref(refline); + if (objIndex.getID() == 0 && objIndex.getGeneration() == 65535) { // The highest generation number possible + refstart--; + } + } + } catch (Exception e) {// in case of error ignore + } + this.buf.position(startPos); + } + + // extend the objIdx table, if necessary + if (refstart + reflen >= this.objIdx.length) { + PDFXref nobjIdx[] = new PDFXref[refstart + reflen]; + System.arraycopy(this.objIdx, 0, nobjIdx, 0, this.objIdx.length); + this.objIdx = nobjIdx; + } + + // read reference lines + for (int refID = refstart; refID < refstart + reflen; refID++) { + // each reference line is 20 bytes long + byte[] refline = new byte[20]; + this.buf.get(refline); + + // ignore this line if the object ID is already defined + if (this.objIdx[refID] != null) { + continue; + } + + // see if it's an active object + if (refline[17] == 'n') { + this.objIdx[refID] = new PDFXref(refline); + } else { + this.objIdx[refID] = new PDFXref(null); + } + } + } + + // at this point, the "trailer" word (not EOL) has been read. + PDFObject trailerdict = readObject(-1, -1, IdentityDecrypter.getInstance()); + if (trailerdict.getType() != PDFObject.DICTIONARY) { + throw new IOException("Expected dictionary after \"trailer\""); + } + + // read the root object location + if (this.root == null) { + this.root = trailerdict.getDictRef("Root"); + if (this.root != null) { + this.root.setObjectId(PDFObject.OBJ_NUM_TRAILER, + PDFObject.OBJ_NUM_TRAILER); + } + } + + // read the encryption information + if (this.encrypt == null) { + this.encrypt = trailerdict.getDictRef("Encrypt"); + if (this.encrypt != null) { + this.encrypt.setObjectId(PDFObject.OBJ_NUM_TRAILER, + PDFObject.OBJ_NUM_TRAILER); + } + + if (this.encrypt != null && !PDFDecrypterFactory.isFilterExist(this.encrypt)) { + this.encrypt = null; // the filter is not located at this trailer, we will try later again + } else { + newDefaultDecrypter = PDFDecrypterFactory.createDecryptor(this.encrypt, trailerdict.getDictRef("ID"), password); + } + } + + + if (this.info == null) { + this.info = trailerdict.getDictRef("Info"); + if (this.info != null) { + if (!this.info.isIndirect()) { + throw new PDFParseException( + "Info in trailer must be an indirect reference"); + } + this.info.setObjectId(PDFObject.OBJ_NUM_TRAILER, + PDFObject.OBJ_NUM_TRAILER); + } + } + + // support for hybrid-PDFs containing an additional compressed-xref-stream + PDFObject xrefstmPos = trailerdict.getDictRef("XRefStm"); + if (xrefstmPos != null) { + int pos14 = this.buf.position(); + this.buf.position(xrefstmPos.getIntValue()); + readTrailer15(password); + this.buf.position(pos14); + } + + // read the location of the previous xref table + PDFObject prevloc = trailerdict.getDictRef("Prev"); + if (prevloc != null) { + this.buf.position(prevloc.getIntValue()); + } else { + break; + } + // see if we have an optional Version entry + + + if (this.root.getDictRef("Version") != null) { + processVersion(this.root.getDictRef("Version").getStringValue()); + } + } + + // make sure we found a root + if (this.root == null) { + throw new PDFParseException("No /Root key found in trailer dictionary"); + } + + if (this.encrypt != null && newDefaultDecrypter!=null) { + PDFObject permissions = this.encrypt.getDictRef("P"); + if (permissions!=null && !newDefaultDecrypter.isOwnerAuthorised()) { + int perms= permissions != null ? permissions.getIntValue() : 0; + if (permissions!=null) { + this.printable = (perms & 4) != 0; + this.saveable = (perms & 16) != 0; + } + } + // Install the new default decrypter only after the trailer has + // been read, as nothing we're reading passing through is encrypted + this.defaultDecrypter = newDefaultDecrypter; + } + + // dereference the root object + this.root.dereference(); + } + + /** + * read the cross reference table from a PDF file. When this method + * is called, the file pointer must point to the start of the word + * "xref" in the file. Reads the xref table and the trailer dictionary. + * If dictionary has a /Prev entry, move file pointer + * and read new trailer + * @param password + */ + private void readTrailer15(PDFPassword password) + throws + IOException, + PDFAuthenticationFailureException, + EncryptionUnsupportedByProductException, + EncryptionUnsupportedByPlatformException { + + // the table of xrefs + // objIdx is initialized from readTrailer(), do not overwrite here data from hybrid PDFs +// objIdx = new PDFXref[50]; + PDFDecrypter newDefaultDecrypter = null; + + while (true) { + PDFObject xrefObj = readObject(-1, -1, IdentityDecrypter.getInstance()); + if (xrefObj == null) { + break; + } + HashMap trailerdict = xrefObj.getDictionary(); + if (trailerdict == null) { + break; + } + PDFObject pdfObject = trailerdict.get("W"); + if (pdfObject == null) { + break; + } + PDFObject[] wNums = pdfObject.getArray(); + int l1 = wNums[0].getIntValue(); + int l2 = wNums[1].getIntValue(); + int l3 = wNums[2].getIntValue(); + + int size = trailerdict.get("Size").getIntValue(); + + byte[] strmbuf = xrefObj.getStream(); + int strmPos = 0; + + PDFObject idxNums = trailerdict.get("Index"); + int[] idxArray; + if (idxNums == null) { + idxArray = new int[]{0, size}; + } + else { + PDFObject[] idxNumArr = idxNums.getArray(); + idxArray = new int[idxNumArr.length]; + for (int i = 0; i < idxNumArr.length; i++) { + idxArray[i] = idxNumArr[i].getIntValue(); + } + } + int idxLen = idxArray.length; + int idxPos = 0; + + + while (idxPos= this.objIdx.length) { + PDFXref nobjIdx[] = new PDFXref[refstart + reflen]; + System.arraycopy(this.objIdx, 0, nobjIdx, 0, this.objIdx.length); + this.objIdx = nobjIdx; + } + + // read reference lines + for (int refID = refstart; refID < refstart + reflen; refID++) { + + int type = readNum(strmbuf, strmPos, l1); + strmPos += l1; + int id = readNum(strmbuf, strmPos, l2); + strmPos += l2; + int gen = readNum(strmbuf, strmPos, l3); + strmPos += l3; + + // ignore this line if the object ID is already defined + if (this.objIdx[refID] != null) { + continue; + } + + // see if it's an active object + if (type == 0) { // inactive + this.objIdx[refID] = new PDFXref(null); + } else if (type == 1) { // active uncompressed + this.objIdx[refID] = new PDFXref(id, gen); + } else { // active compressed + this.objIdx[refID] = new PDFXref(id, gen, true); + } + + } + } + + // read the root object location + if (this.root == null) { + this.root = trailerdict.get("Root"); + if (this.root != null) { + this.root.setObjectId(PDFObject.OBJ_NUM_TRAILER, + PDFObject.OBJ_NUM_TRAILER); + } + } + + // read the encryption information + if (this.encrypt == null) { + this.encrypt = trailerdict.get("Encrypt"); + if (this.encrypt != null) { + this.encrypt.setObjectId(PDFObject.OBJ_NUM_TRAILER, + PDFObject.OBJ_NUM_TRAILER); + + } + if (this.encrypt != null && !PDFDecrypterFactory.isFilterExist(this.encrypt)) { + this.encrypt = null; // the filter is not located at this trailer, we will try later again + } else { + newDefaultDecrypter = PDFDecrypterFactory.createDecryptor(this.encrypt, trailerdict.get("ID"), password); + } + } + + if (this.info == null) { + this.info = trailerdict.get("Info"); + if (this.info != null) { + if (!this.info.isIndirect()) { + throw new PDFParseException( + "Info in trailer must be an indirect reference"); + } + this.info.setObjectId(PDFObject.OBJ_NUM_TRAILER, + PDFObject.OBJ_NUM_TRAILER); + } + } + + // read the location of the previous xref table + PDFObject prevloc = trailerdict.get("Prev"); + if (prevloc != null) { + this.buf.position(prevloc.getIntValue()); + } else { + break; + } + // see if we have an optional Version entry + + + if (this.root.getDictRef("Version") != null) { + processVersion(this.root.getDictRef("Version").getStringValue()); + } + } + + // make sure we found a root + if (this.root == null) { + throw new PDFParseException("No /Root key found in trailer dictionary"); + } + + // check what permissions are relevant + if (this.encrypt != null && newDefaultDecrypter!=null) { + PDFObject permissions = this.encrypt.getDictRef("P"); + if (permissions!=null && !newDefaultDecrypter.isOwnerAuthorised()) { + int perms= permissions != null ? permissions.getIntValue() : 0; + if (permissions!=null) { + this.printable = (perms & 4) != 0; + this.saveable = (perms & 16) != 0; + } + } + // Install the new default decrypter only after the trailer has + // been read, as nothing we're reading passing through is encrypted + this.defaultDecrypter = newDefaultDecrypter; + } + + // dereference the root object + this.root.dereference(); + } + + private int readNum(byte[] sbuf, int pos, int numBytes) { + int result = 0; + for (int i=0; i= 0) { + this.buf.position(scanPos); + this.buf.get(scan); + + // find startxref in scan + String scans = new String(scan); + loc = scans.indexOf("startxref"); + if (loc >= 0) { + if (scanPos + loc + scan.length <= this.buf.limit()) { + scanPos = scanPos + loc; + loc = 0; + } + + break; + } + scanPos -= scan.length - 10; + } + + if (scanPos < 0) { + throw new IOException("This may not be a PDF File"); + } + + this.buf.position(scanPos); + this.buf.get(scan); + String scans = new String(scan); + + loc += 10; // skip over "startxref" and first EOL char + if (scans.charAt(loc) < 32) { + loc++; + } // skip over possible 2nd EOL char + while (scans.charAt(loc) == 32) { + loc++; + } // skip over possible leading blanks + // read number + int numstart = loc; + while (loc < scans.length() && + scans.charAt(loc) >= '0' && + scans.charAt(loc) <= '9') { + loc++; + } + int xrefpos = Integer.parseInt(scans.substring(numstart, loc)); + this.buf.position(xrefpos); + + try { + readTrailer(password); + } catch (UnsupportedEncryptionException e) { + throw new PDFParseException(e.getMessage(), e); + } + } + + /** + * Gets the outline tree as a tree of OutlineNode, which is a subclass + * of DefaultMutableTreeNode. If there is no outline tree, this method + * returns null. + */ + public OutlineNode getOutline() throws IOException { + // find the outlines entry in the root object + PDFObject oroot = this.root.getDictRef("Outlines"); + OutlineNode work = null; + OutlineNode outline = null; + if (oroot != null) { + // find the first child of the outline root + PDFObject scan = oroot.getDictRef("First"); + outline = work = new OutlineNode(""); + + // scan each sibling in turn + while (scan != null) { + // add the new node with it's name + String title = scan.getDictRef("Title").getTextStringValue(); + OutlineNode build = new OutlineNode(title); + work.add(build); + + // find the action + PDFAction action = null; + + PDFObject actionObj = scan.getDictRef("A"); + if (actionObj != null) { + try { + action = PDFAction.getAction(actionObj, getRoot()); + } + catch (PDFParseException e) { + // oh well + } + } else { + // try to create an action from a destination + PDFObject destObj = scan.getDictRef("Dest"); + if (destObj != null) { + try { + PDFDestination dest = + PDFDestination.getDestination(destObj, getRoot()); + + action = new GoToAction(dest); + } catch (IOException ioe) { + // oh well + } + } + } + + // did we find an action? If so, add it + if (action != null) { + build.setAction(action); + } + + // find the first child of this node + PDFObject kid = scan.getDictRef("First"); + if (kid != null) { + work = build; + scan = kid; + } else { + // no child. Process the next sibling + PDFObject next = scan.getDictRef("Next"); + while (next == null) { + scan = scan.getDictRef("Parent"); + next = scan.getDictRef("Next"); + work = (OutlineNode) work.getParent(); + if (work == null) { + break; + } + } + scan = next; + } + } + } + + return outline; + } + + /** + * Gets the page number (starting from 1) of the page represented by + * a particular PDFObject. The PDFObject must be a Page dictionary or + * a destination description (or an action). + * @return a number between 1 and the number of pages indicating the + * page number, or 0 if the PDFObject is not in the page tree. + */ + public int getPageNumber(PDFObject page) throws IOException { + if (page.getType() == PDFObject.ARRAY) { + page = page.getAt(0); + } + + // now we've got a page. Make sure. + PDFObject typeObj = page.getDictRef("Type"); + if (typeObj == null || !typeObj.getStringValue().equals("Page")) { + return 0; + } + + int count = 0; + while (true) { + PDFObject parent = page.getDictRef("Parent"); + if (parent == null) { + break; + } + PDFObject kids[] = parent.getDictRef("Kids").getArray(); + for (int i = 0; i < kids.length; i++) { + if (kids[i].equals(page)) { + break; + } else { + PDFObject kcount = kids[i].getDictRef("Count"); + if (kcount != null) { + count += kcount.getIntValue(); + } else { + count += 1; + } + } + } + page = parent; + } + return count; + } + + /** + * Get the page commands for a given page in a separate thread. + * + * @param pagenum the number of the page to get commands for + */ + public PDFPage getPage(int pagenum) { + return getPage(pagenum, false); + } + + /** + * Get the page commands for a given page. + * + * @param pagenum the number of the page to get commands for + * @param wait if true, do not exit until the page is complete. + */ + public PDFPage getPage(int pagenum, boolean wait) { + Integer key = Integer.valueOf(pagenum); + HashMap resources = null; + PDFObject pageObj = null; + + PDFPage page = this.cache.getPage(key); + PDFParser parser = this.cache.getPageParser(key); + if (page == null) { + try { + // hunt down the page! + resources = new HashMap(); + + PDFObject topPagesObj = this.root.getDictRef("Pages"); + pageObj = findPage(topPagesObj, 0, pagenum, resources); + + if (pageObj == null) { + return null; + } + + page = createPage(pagenum, pageObj); + + byte[] stream = getContents(pageObj); + parser = new PDFParser(page, stream, resources); + + this.cache.addPage(key, page, parser); + } catch (IOException ioe) { + return null; + } + } + + if (parser != null) { + if (!parser.isFinished()) { + parser.go(wait); + } + if (parser.getStatus() == Watchable.ERROR) { + PDFDebugger.debug("Error in parsing the PDF page!"); + } + } + + return page; + } + + /** + * Stop the rendering of a particular image on this page + */ + public void stop(int pageNum) { + PDFParser parser = this.cache.getPageParser(Integer.valueOf(pageNum)); + if (parser != null) { + // stop it + parser.stop(); + } + } + + /** + * get the stream representing the content of a particular page. + * + * @param pageObj the page object to get the contents of + * @return a concatenation of any content streams for the requested + * page. + */ + private byte[] getContents(PDFObject pageObj) throws IOException { + // concatenate all the streams + PDFObject contentsObj = pageObj.getDictRef("Contents"); + if (contentsObj == null) { + return new byte[0]; + } + + PDFObject contents[] = contentsObj.getArray(); + + // see if we have only one stream (the easy case) + if (contents.length == 1) { + return contents[0].getStream(); + } + + // first get the total length of all the streams + int len = 0; + for (int i = 0; i < contents.length; i++) { + byte[] data = contents[i].getStream(); + if (data == null) { + throw new PDFParseException("No stream on content " + i + + ": " + contents[i]); + } + len += data.length; + } + + // now assemble them all into one object + byte[] stream = new byte[len]; + len = 0; + for (int i = 0; i < contents.length; i++) { + byte data[] = contents[i].getStream(); + System.arraycopy(data, 0, stream, len, data.length); + len += data.length; + } + + return stream; + } + + /** + * Create a PDF Page object by finding the relevant inherited + * properties + * + * @param pageObj the PDF object for the page to be created + */ + private PDFPage createPage(int pagenum, PDFObject pageObj) + throws IOException { + int rotation = 0; + Rectangle2D mediabox = null; // third choice, if no crop + Rectangle2D cropbox = null; // second choice + Rectangle2D trimbox = null; // first choice + + PDFObject mediaboxObj = getInheritedValue(pageObj, "MediaBox"); + if (mediaboxObj != null) { + mediabox = parseNormalisedRectangle(mediaboxObj); + } + + PDFObject cropboxObj = getInheritedValue(pageObj, "CropBox"); + if (cropboxObj != null) { + cropbox = parseNormalisedRectangle(cropboxObj); + } + + PDFObject trimboxObj = getInheritedValue(pageObj, "TrimBox"); + if (trimboxObj != null) { + trimbox = parseNormalisedRectangle(trimboxObj); + } + + PDFObject rotateObj = getInheritedValue(pageObj, "Rotate"); + if (rotateObj != null) { + rotation = rotateObj.getIntValue(); + } + + // read annotations and add them to the PDF page + PDFObject annots = getInheritedValue(pageObj, "Annots"); + List annotationList = new ArrayList(); + if (annots != null) { + if (annots.getType() != PDFObject.ARRAY) { + throw new PDFParseException("Can't parse annotations: " + annots.toString()); + } + PDFObject[] array = annots.getArray(); + for (PDFObject object : array) { + try { + PDFAnnotation pdfAnnot = PDFAnnotation.createAnnotation(object); + if(pdfAnnot != null) { + annotationList.add(pdfAnnot); + } + }catch (PDFParseException e) { + // do nothing, annotations could not be parsed and links will not be displayed. + } + } + } + + Rectangle2D bbox = (trimbox == null ? ((cropbox == null) ? mediabox : cropbox) : trimbox); + PDFPage page = new PDFPage(pagenum, bbox, rotation, this.cache); + page.setAnnots(annotationList); + return page; + } + + /** + * Get the PDFObject representing the content of a particular page. Note + * that the number of the page need not have anything to do with the + * label on that page. If there are two blank pages, and then roman + * numerals for the page number, then passing in 6 will get page (iv). + * + * @param pagedict the top of the pages tree + * @param start the page number of the first page in this dictionary + * @param getPage the number of the page to find; NOT the page's label. + * @param resources a HashMap that will be filled with any resource + * definitions encountered on the search for the page + */ + private PDFObject findPage(PDFObject pagedict, int start, int getPage, + Map resources) throws IOException { + PDFObject rsrcObj = pagedict.getDictRef("Resources"); + if (rsrcObj != null) { + resources.putAll(rsrcObj.getDictionary()); + } + + PDFObject typeObj = pagedict.getDictRef("Type"); + if (typeObj != null && typeObj.getStringValue().equals("Page")) { + // we found our page! + return pagedict; + } + + // find the first child for which (start + count) > getPage + PDFObject kidsObj = pagedict.getDictRef("Kids"); + if (kidsObj != null) { + PDFObject[] kids = kidsObj.getArray(); + for (int i = 0; i < kids.length; i++) { + int count = 1; + // BUG: some PDFs (T1Format.pdf) don't have the Type tag. + // use the Count tag to indicate a Pages dictionary instead. + PDFObject countItem = kids[i].getDictRef("Count"); + // if (kids[i].getDictRef("Type").getStringValue().equals("Pages")) { + if (countItem != null) { + count = countItem.getIntValue(); + } + + if (start + count >= getPage) { + return findPage(kids[i], start, getPage, resources); + } + + start += count; + } + } + + return null; + } + + /** + * Find a property value in a page that may be inherited. If the value + * is not defined in the page itself, follow the page's "parent" links + * until the value is found or the top of the tree is reached. + * + * @param pageObj the object representing the page + * @param propName the name of the property we are looking for + */ + private PDFObject getInheritedValue(PDFObject pageObj, String propName) + throws IOException { + // see if we have the property + PDFObject propObj = pageObj.getDictRef(propName); + if (propObj != null) { + return propObj; + } + + // recursively see if any of our parent have it + PDFObject parentObj = pageObj.getDictRef("Parent"); + if (parentObj != null) { + return getInheritedValue(parentObj, propName); + } + + // no luck + return null; + } + + public static Rectangle2D parseNormalisedRectangle(PDFObject obj) + throws IOException { + + if (obj != null) { + if (obj.getType() == PDFObject.ARRAY) { + PDFObject bounds[] = obj.getArray(); + if (bounds.length == 4) { + final double x0 = bounds[0].getDoubleValue(); + final double y0 = bounds[1].getDoubleValue(); + final double x1 = bounds[2].getDoubleValue(); + final double y1 = bounds[3].getDoubleValue(); + + final double minX; + final double maxY; + final double maxX; + final double minY; + + if (x0 < x1) { + minX = x0; + maxX = x1; + } else { + minX = x1; + maxX = x0; + } + if (y0 < y1) { + minY = y0; + maxY = y1; + } else { + minY = y1; + maxY = y0; + } + + return new Rectangle2D.Double(minX, minY, Math.abs(maxX - minX), Math.abs(maxY - minY)); + + } else { + throw new PDFParseException("Rectangle definition didn't have 4 elements"); + } + } else { + throw new PDFParseException("Rectangle definition not an array"); + } + } else { + throw new PDFParseException("Rectangle not present"); + } + + } + + /** + * Get the default decrypter for the document + * @return the default decrypter; never null, even for documents that + * aren't encrypted + */ + public PDFDecrypter getDefaultDecrypter() { + return this.defaultDecrypter; + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFImage.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFImage.java new file mode 100644 index 0000000000..5bfe5f360c --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFImage.java @@ -0,0 +1,231 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer; + +import java.awt.*; +import java.awt.image.*; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Map; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; + +import com.github.librepdf.pdfrenderer.colorspace.IndexedColor; +import com.github.librepdf.pdfrenderer.colorspace.PDFColorSpace; +import com.github.librepdf.pdfrenderer.decode.PDFDecoder; + +public class PDFImage { + + private int[] colorKeyMask = null; + private int width; + private int height; + private PDFColorSpace colorSpace; + private int bpc; + private boolean imageMask = false; + private PDFImage sMask; + private float[] decode; + private final PDFObject imageObj; + private final boolean jpegDecode; + + protected PDFImage(PDFObject imageObj) throws IOException { + this.imageObj = imageObj; + this.jpegDecode = PDFDecoder.isLastFilter(imageObj, PDFDecoder.DCT_FILTERS); + } + + public static PDFImage createImage(PDFObject obj, Map resources, boolean useAsSMask) + throws IOException { + PDFImage image = new PDFImage(obj); + image.setWidth(obj.getDictRef("Width").getIntValue()); + image.setHeight(obj.getDictRef("Height").getIntValue()); + + if (obj.getDictRef("ImageMask") != null) { + image.setImageMask(obj.getDictRef("ImageMask").getBooleanValue()); + image.setBitsPerComponent(1); + image.setColorSpace(new IndexedColor(useAsSMask ? new Color[]{Color.WHITE, Color.BLACK} : new Color[]{Color.BLACK, Color.WHITE})); + } else { + image.setBitsPerComponent(obj.getDictRef("BitsPerComponent").getIntValue()); + image.setColorSpace(PDFColorSpace.getColorSpace(obj.getDictRef("ColorSpace"), resources)); + if (obj.getDictRef("Decode") != null) { + image.setDecode(parseDecodeArray(obj.getDictRef("Decode").getArray())); + } + if (obj.getDictRef("SMask") != null) { + image.setSMask(PDFImage.createImage(obj.getDictRef("SMask"), resources, true)); + } + } + return image; + } + + public BufferedImage getImage() throws PDFImageParseException { + try { + BufferedImage bi = (BufferedImage) this.imageObj.getCache(); + if (bi == null) { + byte[] data = imageObj.getStream(); + ByteBuffer jpegBytes = this.jpegDecode ? imageObj.getStreamBuffer(PDFDecoder.DCT_FILTERS) : null; + bi = parseData(data, jpegBytes); + this.imageObj.setCache(bi); + } + return bi; + } catch (IOException ioe) { + throw new PDFImageParseException("Error reading image: " + ioe.getMessage(), ioe); + } + } + + protected BufferedImage parseData(byte[] data, ByteBuffer jpegData) throws IOException { + ColorModel cm = createColorModel(); + BufferedImage bi; + + if (jpegData != null) { + bi = decodeJPEG(jpegData, cm); + } else { + DataBuffer db = new DataBufferByte(data, data.length); + + // Check if the data array is large enough + SampleModel sm = cm.createCompatibleSampleModel(getWidth(), getHeight()); + int expectedSize = sm.getWidth() * sm.getHeight() * sm.getNumBands(); + if (data.length < expectedSize) { + throw new IOException("Data array too small, expected at least " + expectedSize + " bytes."); + } + + WritableRaster raster = Raster.createWritableRaster(sm, db, null); + bi = new BufferedImage(cm, raster, true, null); + } + + if (sMask != null) { + bi = applySMask(bi, sMask.getImage()); + } + + return bi; + } + + + private BufferedImage decodeJPEG(ByteBuffer jpegData, ColorModel cm) throws IOException { + ImageReader jpegReader = ImageIO.getImageReadersByFormatName("jpeg").next(); + jpegReader.setInput(ImageIO.createImageInputStream(new ByteBufferInputStream(jpegData)), true, false); + ImageReadParam param = new ImageReadParam(); + param.setDestination(new BufferedImage(cm, cm.createCompatibleWritableRaster(getWidth(), getHeight()), true, null)); + return jpegReader.read(0, param); + } + + private BufferedImage applySMask(BufferedImage bi, BufferedImage smask) { + int w = bi.getWidth(); + int h = bi.getHeight(); + BufferedImage combined = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int rgb = bi.getRGB(x, y); + int alpha = smask.getRGB(x, y) & 0xFF; + combined.setRGB(x, y, (alpha << 24) | (rgb & 0xFFFFFF)); + } + } + return combined; + } + + private ColorModel createColorModel() { + if (colorSpace instanceof IndexedColor) { + return createIndexedColorModel((IndexedColor) colorSpace); + } else { + int[] bits = new int[colorSpace.getNumComponents()]; + for (int i = 0; i < bits.length; i++) { + bits[i] = bpc; + } + return new ComponentColorModel(colorSpace.getColorSpace(), bits, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE); + } + } + + private ColorModel createIndexedColorModel(IndexedColor ics) { + byte[] components = ics.getColorComponents(); + int num = ics.getCount(); + + if (decode != null) { + components = applyDecodeArray(components, decode); + } + + return new IndexColorModel(bpc, num, components, 0, false); + } + + private static byte[] applyDecodeArray(byte[] components, float[] decode) { + byte[] normComps = new byte[components.length]; + for (int i = 0; i < components.length / 3; i++) { + int idx = (int) ((decode[i * 2] * (components.length - 1)) / (decode[(i * 2) + 1] - decode[i * 2])); + normComps[i * 3] = components[idx * 3]; + normComps[(i * 3) + 1] = components[(idx * 3) + 1]; + normComps[(i * 3) + 2] = components[(idx * 3) + 2]; + } + return normComps; + } + + private static float[] parseDecodeArray(PDFObject[] decodeArray) { + float[] decode = new float[decodeArray.length]; + for (int i = 0; i < decodeArray.length; i++) { + try { + decode[i] = decodeArray[i].getFloatValue(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return decode; + } + + public int getWidth() { + return this.width; + } + + protected void setWidth(int width) { + this.width = width; + } + + public int getHeight() { + return this.height; + } + + protected void setHeight(int height) { + this.height = height; + } + + protected void setColorSpace(PDFColorSpace colorSpace) { + this.colorSpace = colorSpace; + } + + protected void setBitsPerComponent(int bpc) { + this.bpc = bpc; + } + + public boolean isImageMask() { + return this.imageMask; + } + + public void setImageMask(boolean imageMask) { + this.imageMask = imageMask; + } + + public PDFImage getSMask() { + return this.sMask; + } + + protected void setSMask(PDFImage sMask) { + this.sMask = sMask; + } + + protected void setDecode(float[] decode) { + this.decode = decode; + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFImageParseException.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFImageParseException.java new file mode 100644 index 0000000000..dde56c72e2 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFImageParseException.java @@ -0,0 +1,18 @@ +package com.github.librepdf.pdfrenderer; + +import java.io.IOException; + +/** + * an exception class for recording errors when parsing an PDFImage + * @author Katja Sondermann + */ +public class PDFImageParseException extends IOException { + public PDFImageParseException(String msg) { + super(msg); + } + + public PDFImageParseException(String msg, Throwable cause) { + this(msg); + initCause(cause); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFObject.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFObject.java new file mode 100644 index 0000000000..6d4747711b --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFObject.java @@ -0,0 +1,849 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer; + +import java.io.IOException; +import java.lang.ref.SoftReference; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import com.github.librepdf.pdfrenderer.decode.PDFDecoder; +import com.github.librepdf.pdfrenderer.decrypt.IdentityDecrypter; +import com.github.librepdf.pdfrenderer.decrypt.PDFDecrypter; + +/** + * a class encapsulating all the possibilities of content for + * an object in a PDF file. + *

+ * A PDF object can be a simple type, like a Boolean, a Number, + * a String, or the Null value. It can also be a NAME, which + * looks like a string, but is a special type in PDF files, like + * "/Name". + *

+ * A PDF object can also be complex types, including Array; + * Dictionary; Stream, which is a Dictionary plus an array of + * bytes; or Indirect, which is a reference to some other + * PDF object. Indirect references will always be dereferenced + * by the time any data is returned from one of the methods + * in this class. + * + * @author Mike Wessler + */ +public class PDFObject { + + /** an indirect reference*/ + public static final int INDIRECT = 0; // PDFXref + /** a Boolean */ + public static final int BOOLEAN = 1; // Boolean + /** a Number, represented as a double */ + public static final int NUMBER = 2; // Double + /** a String */ + public static final int STRING = 3; // String + /** a special string, seen in PDF files as /Name */ + public static final int NAME = 4; // String + /** an array of PDFObjects */ + public static final int ARRAY = 5; // Array of PDFObject + /** a Hashmap that maps String names to PDFObjects */ + public static final int DICTIONARY = 6; // HashMap(String->PDFObject) + /** a Stream: a Hashmap with a byte array */ + public static final int STREAM = 7; // HashMap + byte[] + /** the NULL object (there is only one) */ + public static final int NULL = 8; // null + /** a special PDF bare word, like R, obj, true, false, etc */ + public static final int KEYWORD = 9; // String + /** + * When a value of {@link #getObjGen objNum} or {@link #getObjGen objGen}, + * indicates that the object is not top-level, and is embedded in another + * object + */ + public static final int OBJ_NUM_EMBEDDED = -2; + + /** + * When a value of {@link #getObjGen objNum} or {@link #getObjGen objGen}, + * indicates that the object is not top-level, and is embedded directly + * in the trailer. + */ + public static final int OBJ_NUM_TRAILER = -1; + + /** the NULL PDFObject */ + public static final PDFObject nullObj = new PDFObject(null, NULL, null); + /** the type of this object */ + private int type; + /** the value of this object. It can be a wide number of things, defined by type */ + private Object value; + /** the encoded stream, if this is a STREAM object */ + private ByteBuffer stream; + /** a cached version of the decoded stream */ + private SoftReference decodedStream; + /** The filter limits used to generate the cached decoded stream */ + private Set decodedStreamFilterLimits = null; + /** + * the PDFFile from which this object came, used for + * dereferences + */ + private final PDFFile owner; + /** + * a cache of translated data. This data can be + * garbage collected at any time, after which it will + * have to be rebuilt. + */ + private SoftReference cache; + + /** @see #getObjNum() */ + private int objNum = OBJ_NUM_EMBEDDED; + + /** @see #getObjGen() */ + private int objGen = OBJ_NUM_EMBEDDED; + + /** + * create a new simple PDFObject with a type and a value + * @param owner the PDFFile in which this object resides, used + * for dereferencing. This may be null. + * @param type the type of object + * @param value the value. For DICTIONARY, this is a HashMap. + * for ARRAY it's an ArrayList. For NUMBER, it's a Double. + * for BOOLEAN, it's Boolean.TRUE or Boolean.FALSE. For + * everything else, it's a String. + */ + public PDFObject(PDFFile owner, int type, Object value) { + this.type = type; + if (type == NAME) { + value = ((String) value).intern(); + } else if (type == KEYWORD && value.equals("true")) { + this.type = BOOLEAN; + value = Boolean.TRUE; + } else if (type == KEYWORD && value.equals("false")) { + this.type = BOOLEAN; + value = Boolean.FALSE; + } + this.value = value; + this.owner = owner; + } + + /** + * create a new PDFObject that is the closest match to a + * given Java object. Possibilities include Double, String, + * PDFObject[], HashMap, Boolean, or PDFParser.Tok, + * which should be "true" or "false" to turn into a BOOLEAN. + * + * @param obj the sample Java object to convert to a PDFObject. + * @throws PDFParseException if the object isn't one of the + * above examples, and can't be turned into a PDFObject. + */ + public PDFObject(Object obj) throws PDFParseException { + this.owner = null; + this.value = obj; + if ((obj instanceof Double) || (obj instanceof Integer)) { + this.type = NUMBER; + } else if (obj instanceof String) { + this.type = NAME; + } else if (obj instanceof PDFObject[]) { + this.type = ARRAY; + } else if (obj instanceof Object[]) { + Object[] srcary = (Object[]) obj; + PDFObject[] dstary = new PDFObject[srcary.length]; + for (int i = 0; i < srcary.length; i++) { + dstary[i] = new PDFObject(srcary[i]); + } + value = dstary; + this.type = ARRAY; + } else if (obj instanceof HashMap) { + this.type = DICTIONARY; + } else if (obj instanceof Boolean) { + this.type = BOOLEAN; + } else if (obj instanceof PDFParser.Tok) { + PDFParser.Tok tok = (PDFParser.Tok) obj; + if (tok!=null && tok.name!=null && tok.name.equals("true")) { + this.value = Boolean.TRUE; + this.type = BOOLEAN; + } else if (tok!=null && tok.name!=null && tok.name.equals("false")) { + this.value = Boolean.FALSE; + this.type = BOOLEAN; + } else { + this.value = tok.name; + this.type = NAME; + } + } else { + throw new PDFParseException("Bad type for raw PDFObject: " + obj); + } + } + + /** + * create a new PDFObject based on a PDFXref + * @param owner the PDFFile from which the PDFXref was drawn + * @param xref the PDFXref to turn into a PDFObject + */ + public PDFObject(PDFFile owner, PDFXref xref) { + this.type = INDIRECT; + this.value = xref; + this.owner = owner; + } + + /** + * Convenient method to get a dictionary value as String + * @param name of the dictionary value + * @return the value or null if no entry exists with that name + * @throws IOException + */ + public String getDictRefAsString(String name) throws IOException { + PDFObject ref = getDictRef(name); + return ref == null ? null : ref.getStringValue(); + } + + /** + * Convenient method to get a dictionary value as String + * @param name of the dictionary value + * @return the value or null if no entry exists with that name + * @throws IOException + */ + public Boolean getDictRefAsBoolean(String name) throws IOException { + PDFObject ref = getDictRef(name); + return ref == null ? null : ref.getBooleanValue(); + } + + /** + * Convenient method to get a dictionary value as Integer + * @param name of the dictionary value + * @return the value or null if no entry exists with that name + * @throws IOException + */ + public Integer getDictRefAsInt(String name) throws IOException { + PDFObject ref = getDictRef(name); + return ref == null ? null : ref.getIntValue(); + } + + /** + * Convenient method to get a dictionary value as int[] + * @param name of the dictionary value + * @return the value or null if no entry exists with that name + * @throws IOException + */ + public int[] getDictRefAsIntArray(String name) throws IOException { + PDFObject ref = getDictRef(name); + if (ref == null) { + return null; + } + PDFObject[] values = ref.getArray(); + int[] result = new int[values.length]; + for (int i = 0; i < values.length; i++) { + result[i] = values[i].getIntValue(); + } + return result; + } + + /** + * Convenient method to get a dictionary value as float[] + * @param name of the dictionary value + * @return the value or null if no entry exists with that name + * @throws IOException + */ + public float[] getDictRefAsFloatArray(String name) throws IOException { + PDFObject ref = getDictRef(name); + if (ref == null) { + return null; + } + PDFObject[] values = ref.getArray(); + float[] result = new float[values.length]; + for (int i = 0; i < values.length; i++) { + result[i] = values[i].getFloatValue(); + } + return result; + } + + + /** + * Convenient method to get a dictionary value as Float + * @param name of the dictionary value + * @return the value or null if no entry exists with that name + * @throws IOException + */ + public Float getDictRefAsFloat(String name) throws IOException { + PDFObject ref = getDictRef(name); + return ref == null ? null : ref.getFloatValue(); + } + + /** + * Convenient method to get a dictionary value as Double + * @param name of the dictionary value + * @return the value or null if no entry exists with that name + * @throws IOException + */ + public Double getDictRefAsDouble(String name) throws IOException { + PDFObject ref = getDictRef(name); + return ref == null ? null : ref.getDoubleValue(); + } + + /** + * get the type of this object. The object will be + * dereferenced, so INDIRECT will never be returned. + * @return the type of the object + */ + public int getType() throws IOException { + if (type == INDIRECT) { + return dereference().getType(); + } + + return type; + } + + /** + * set the stream of this object. It should have been + * a DICTIONARY before the call. + * @param data the data, as a ByteBuffer. + */ + public void setStream(ByteBuffer data) { + this.type = STREAM; + this.stream = data; + } + + /** + * get the value in the cache. May become null at any time. + * @return the cached value, or null if the value has been + * garbage collected. + */ + public Object getCache() throws IOException { + if (type == INDIRECT) { + return dereference().getCache(); + } else if (cache != null) { + return cache.get(); + } else { + return null; + } + } + + /** + * set the cached value. The object may be garbage collected + * if no other reference exists to it. + * @param obj the object to be cached + */ + public void setCache(Object obj) throws IOException { + if (type == INDIRECT) { + dereference().setCache(obj); + return; + } else { + cache = new SoftReference(obj); + } + } + + public byte[] getStream(Set filterLimits) throws IOException + { + if (type == INDIRECT) { + return dereference().getStream(filterLimits); + } else if (type == STREAM && stream != null) { + byte[] data = null; + + synchronized (stream) { + // decode + ByteBuffer streamBuf = decodeStream(filterLimits); + // ByteBuffer streamBuf = stream; + + // First try to use the array with no copying. This can only + // be done if the buffer has a backing array, and is not a slice + if (streamBuf.hasArray() && streamBuf.arrayOffset() == 0) { + byte[] ary = streamBuf.array(); + + // make sure there is no extra data in the buffer + if (ary.length == streamBuf.remaining()) { + return ary; + } + } + + // Can't use the direct buffer, so copy the data (bad) + data = new byte[streamBuf.remaining()]; + streamBuf.get(data); + + // return the stream to its starting position + streamBuf.flip(); + } + + return data; + } else if (type == STRING) { + return PDFStringUtil.asBytes(getStringValue()); + } else { + // wrong type + return null; + } + } + + /** + * get the stream from this object. Will return null if this + * object isn't a STREAM. + * @return the stream, or null, if this isn't a STREAM. + */ + public byte[] getStream() throws IOException { + return getStream(Collections.emptySet()); + } + + /** + * get the stream from this object as a byte buffer. Will return null if + * this object isn't a STREAM. + * @return the buffer, or null, if this isn't a STREAM. + */ + public ByteBuffer getStreamBuffer() throws IOException { + return getStreamBuffer(Collections.emptySet()); + } + + /** + * get the stream from this object as a byte buffer. Will return null if + * this object isn't a STREAM. + * @return the buffer, or null, if this isn't a STREAM. + */ + public ByteBuffer getStreamBuffer(Set filterLimits) throws IOException { + if (type == INDIRECT) { + return dereference().getStreamBuffer(filterLimits); + } else if (type == STREAM && stream != null) { + synchronized (stream) { + ByteBuffer streamBuf = decodeStream(filterLimits); + // ByteBuffer streamBuf = stream; + return streamBuf.duplicate(); + } + } else if (type == STRING) { + String src = getStringValue(); + return ByteBuffer.wrap(src.getBytes()); + } + + // wrong type + return null; + } + + /** + * Get the decoded stream value + */ + private ByteBuffer decodeStream(Set filterLimits) throws IOException { + ByteBuffer outStream = null; + + // first try the cache + if (decodedStream != null && filterLimits.equals(decodedStreamFilterLimits)) { + outStream = (ByteBuffer) decodedStream.get(); + } + + // no luck in the cache, do the actual decoding + if (outStream == null) { + stream.rewind(); + outStream = PDFDecoder.decodeStream(this, stream, filterLimits); + decodedStreamFilterLimits = new HashSet(filterLimits); + decodedStream = new SoftReference(outStream); + } + + return outStream; + } + + /** + * get the value as an int. Will return 0 if this object + * isn't a NUMBER. + */ + public int getIntValue() throws IOException { + if (type == INDIRECT) { + return dereference().getIntValue(); + } else if (type == NUMBER) { + return ((Number) value).intValue(); + } + + // wrong type + return 0; + } + + /** + * get the value as a float. Will return 0 if this object + * isn't a NUMBER + */ + public float getFloatValue() throws IOException { + if (type == INDIRECT) { + return dereference().getFloatValue(); + } else if (type == NUMBER) { + return ((Double) value).floatValue(); + } + + // wrong type + return 0; + } + + /** + * get the value as a double. Will return 0 if this object + * isn't a NUMBER. + */ + public double getDoubleValue() throws IOException { + if (type == INDIRECT) { + return dereference().getDoubleValue(); + } else if (type == NUMBER) { + return ((Number) value).doubleValue(); + } + + // wrong type + return 0; + } + + /** + * get the value as a String. Will return null if the object + * isn't a STRING, NAME, or KEYWORD. This method will NOT + * convert a NUMBER to a String. If the string is actually + * a text string (i.e., may be encoded in UTF16-BE or PdfDocEncoding), + * then one should use {@link #getTextStringValue()} or use one + * of the {@link PDFStringUtil} methods on the result from this + * method. The string value represents exactly the sequence of 8 bit + * characters present in the file, decrypted and decoded as appropriate, + * into a string containing only 8 bit character values - that is, each + * char will be between 0 and 255. + */ + public String getStringValue() throws IOException { + if (type == INDIRECT) { + return dereference().getStringValue(); + } else if (type == STRING || type == NAME || type == KEYWORD) { + return (String) value; + } + + // wrong type + return null; + } + + /** + * Get the value as a text string; i.e., a string encoded in UTF-16BE + * or PDFDocEncoding. Simple latin alpha-numeric characters are preserved in + * both these encodings. + * @return the text string value + * @throws IOException + */ + public String getTextStringValue() throws IOException { + return PDFStringUtil.asTextString(getStringValue()); + } + + /** + * get the value as a PDFObject[]. If this object is an ARRAY, + * will return the array. Otherwise, will return an array + * of one element with this object as the element. + */ + public PDFObject[] getArray() throws IOException { + if (type == INDIRECT) { + return dereference().getArray(); + } else if (type == ARRAY) { + PDFObject[] ary = (PDFObject[]) value; + return ary; + } else { + PDFObject[] ary = new PDFObject[1]; + ary[0] = this; + return ary; + } + } + + /** + * get the value as a boolean. Will return false if this + * object is not a BOOLEAN + */ + public boolean getBooleanValue() throws IOException { + if (type == INDIRECT) { + return dereference().getBooleanValue(); + } else if (type == BOOLEAN) { + return value == Boolean.TRUE; + } + + // wrong type + return false; + } + + /** + * if this object is an ARRAY, get the PDFObject at some + * position in the array. If this is not an ARRAY, returns + * null. + */ + public PDFObject getAt(int idx) throws IOException { + if (type == INDIRECT) { + return dereference().getAt(idx); + } else if (type == ARRAY) { + PDFObject[] ary = (PDFObject[]) value; + return ary[idx]; + } + + // wrong type + return null; + } + + /** + * get an Iterator over all the keys in the dictionary. If + * this object is not a DICTIONARY or a STREAM, returns an + * Iterator over the empty list. + */ + public Iterator getDictKeys() throws IOException { + if (type == INDIRECT) { + return dereference().getDictKeys(); + } else if (type == DICTIONARY || type == STREAM) { + return ((HashMap) value).keySet().iterator(); + } + + // wrong type + return new ArrayList().iterator(); + } + + /** + * get the dictionary as a HashMap. If this isn't a DICTIONARY + * or a STREAM, returns null + */ + public HashMap getDictionary() throws IOException { + if (type == INDIRECT) { + return dereference().getDictionary(); + } else if (type == DICTIONARY || type == STREAM) { + return (HashMap) value; + } + + // wrong type + return new HashMap(); + } + + /** + * get the value associated with a particular key in the + * dictionary. If this isn't a DICTIONARY or a STREAM, + * or there is no such key, returns null. + */ + public PDFObject getDictRef(String key) throws IOException { + if (type == INDIRECT) { + return dereference().getDictRef(key); + } else if (type == DICTIONARY || type == STREAM) { + key = key.intern(); + HashMap h = (HashMap) value; + PDFObject obj = (PDFObject) h.get(key.intern()); + return obj; + } + + // wrong type + return null; + } + + /** + * returns true only if this object is a DICTIONARY or a + * STREAM, and the "Type" entry in the dictionary matches a + * given value. + * @param match the expected value for the "Type" key in the + * dictionary + * @return whether the dictionary is of the expected type + */ + public boolean isDictType(String match) throws IOException { + if (type == INDIRECT) { + return dereference().isDictType(match); + } else if (type != DICTIONARY && type != STREAM) { + return false; + } + + PDFObject obj = getDictRef("Type"); + return obj != null && obj.getStringValue().equals(match); + } + + public PDFDecrypter getDecrypter() { + // PDFObjects without owners are always created as part of + // content instructions. Such an object will never have encryption + // applied to it, as the stream that contains it is the + // unit of encryption, with no further encryption being applied + // within. So if someone asks for the decrypter for + // one of these in-stream objects, no decryption should + // ever be applied. This can be seen with inline images. + return owner != null ? + owner.getDefaultDecrypter() : + IdentityDecrypter.getInstance(); + } + + /** + * Set the object identifiers + * @param objNum the object number + * @param objGen the object generation number + */ + public void setObjectId(int objNum, int objGen) { + assert objNum >= OBJ_NUM_TRAILER; + assert objGen >= OBJ_NUM_TRAILER; + this.objNum = objNum; + this.objGen = objGen; + } + + /** + * Get the object number of this object; a negative value indicates that + * the object is not numbered, as it's not a top-level object: if the value + * is {@link #OBJ_NUM_EMBEDDED}, it is because it's embedded within + * another object. If the value is {@link #OBJ_NUM_TRAILER}, it's because + * it's an object from the trailer. + * @return the object number, if positive + */ + public int getObjNum() { + return objNum; + } + + /** + * Get the object generation number of this object; a negative value + * indicates that the object is not numbered, as it's not a top-level + * object: if the value is {@link #OBJ_NUM_EMBEDDED}, it is because it's + * embedded within another object. If the value is {@link + * #OBJ_NUM_TRAILER}, it's because it's an object from the trailer. + * @return the object generation number, if positive + */ + public int getObjGen() { + return objGen; + } + + /** + * return a representation of this PDFObject as a String. + * Does NOT dereference anything: this is the only method + * that allows you to distinguish an INDIRECT PDFObject. + */ + @Override + public String toString() { + try { + if (type == INDIRECT) { + StringBuffer str = new StringBuffer (); + str.append("Indirect to #" + ((PDFXref) value).getID()); + try { + str.append("\n" + dereference().toString()); + } catch (Throwable t) { + str.append(t.toString()); + } + return str.toString(); + } else if (type == BOOLEAN) { + return "Boolean: " + (getBooleanValue() ? "true" : "false"); + } else if (type == NUMBER) { + return "Number: " + getDoubleValue(); + } else if (type == STRING) { + return "String: " + getStringValue(); + } else if (type == NAME) { + return "Name: /" + getStringValue(); + } else if (type == ARRAY) { + return "Array, length=" + ((PDFObject[]) value).length; + } else if (type == DICTIONARY) { + StringBuffer sb = new StringBuffer(); + PDFObject obj = getDictRef("Type"); + if (obj != null) { + sb.append(obj.getStringValue()); + obj = getDictRef("Subtype"); + if (obj == null) { + obj = getDictRef("S"); + } + if (obj != null) { + sb.append("/" + obj.getStringValue()); + } + } else { + sb.append("Untyped"); + } + sb.append(" dictionary. Keys:"); + HashMap hm = (HashMap) value; + Iterator it = hm.entrySet().iterator(); + Map.Entry entry; + while (it.hasNext()) { + entry = (Map.Entry) it.next(); + sb.append("\n " + entry.getKey() + " " + entry.getValue()); + } + return sb.toString(); + } else if (type == STREAM) { + byte[] st = getStream(); + if (st == null) { + return "Broken stream"; + } + return "Stream: [[" + new String(st, 0, st.length > 30 ? 30 : st.length) + "]]"; + } else if (type == NULL) { + return "Null"; + } else if (type == KEYWORD) { + return "Keyword: " + getStringValue(); + /* } else if (type==IMAGE) { + StringBuffer sb= new StringBuffer(); + java.awt.Image im= (java.awt.Image)stream; + sb.append("Image ("+im.getWidth(null)+"x"+im.getHeight(null)+", with keys:"); + HashMap hm= (HashMap)value; + Iterator it= hm.keySet().iterator(); + while(it.hasNext()) { + sb.append(" "+(String)it.next()); + } + return sb.toString();*/ + } else { + return "Whoops! big error! Unknown type"; + } + } catch (IOException ioe) { + return "Caught an error: " + ioe; + } + } + + /** + * Make sure that this object is dereferenced. Use the cache of + * an indirect object to cache the dereferenced value, if possible. + */ + public PDFObject dereference() throws IOException { + if (type == INDIRECT) { + PDFObject obj = null; + + if (cache != null) { + obj = (PDFObject) cache.get(); + } + + if (obj == null || obj.value == null) { + if (owner == null) { + PDFDebugger.debug("Bad seed (owner==null)! Object=" + this); + } + + obj = owner.dereference((PDFXref)value, getDecrypter()); + + cache = new SoftReference(obj); + } + + return obj; + } else { + // not indirect, no need to dereference + return this; + } + } + + /** + * Identify whether the object is currently an indirect/cross-reference + * @return whether currently indirect + */ + public boolean isIndirect() { + return (type == INDIRECT); + } + + /** + * Test whether two PDFObject are equal. Objects are equal IFF they + * are the same reference OR they are both indirect objects with the + * same id and generation number in their xref + */ + @Override + public boolean equals(Object o) { + if (super.equals(o)) { + // they are the same object + return true; + } else if (type == INDIRECT && o instanceof PDFObject) { + // they are both PDFObjects. Check type and xref. + PDFObject obj = (PDFObject) o; + + if (obj.type == INDIRECT) { + PDFXref lXref = (PDFXref) value; + PDFXref rXref = (PDFXref) obj.value; + + return ((lXref.getID() == rXref.getID()) && + (lXref.getGeneration() == rXref.getGeneration())); + } + } + + return false; + } + + /** + * Returns the root of this object. + * @return + */ + public PDFObject getRoot() { + return owner.getRoot(); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFPage.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFPage.java new file mode 100644 index 0000000000..c2d1a3c306 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFPage.java @@ -0,0 +1,965 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Image; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.GeneralPath; +import java.awt.geom.NoninvertibleTransformException; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.awt.image.ImageObserver; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import com.github.librepdf.pdfrenderer.annotation.AnnotationType; +import com.github.librepdf.pdfrenderer.annotation.PDFAnnotation; + +/** +* A PDFPage encapsulates the parsed commands required to render a +* single page from a PDFFile. The PDFPage is not itself drawable; +* instead, create a PDFImage to display something on the screen. +*

+* This file also contains all of the PDFCmd commands that might be a part of the command stream in +* a PDFPage. They probably should be inner classes of PDFPage instead of separate non-public +* classes. +* +* @author Mike Wessler +*/ +public class PDFPage { + /** + * the array of commands. The length of this array will always + * be greater than or equal to the actual number of commands. + */ + private final List commands; + /** + * whether this page has been finished. If true, there will be no + * more commands added to the cmds list. + */ + private boolean finished = false; + /** the page number used to find this page */ + private final int pageNumber; + /** the bounding box of the page, in page coordinates */ + private final Rectangle2D bbox; + /** the rotation of this page, in degrees */ + private final int rotation; + /** + * a map from image info (width, height, clip) to a soft reference to the + * rendered image + */ + private final Cache cache; + /** a map from image info to weak references to parsers that are active */ + public Map> renderers; + /** List of annotations for this page */ + private List annots; + + /** + * create a PDFPage with dimensions in bbox and rotation. + */ + public PDFPage(Rectangle2D bbox, int rotation) { + this(-1, bbox, rotation, null); + } + + /** + * create a PDFPage with dimensions in bbox and rotation. + */ + public PDFPage(int pageNumber, Rectangle2D bbox, int rotation, Cache cache) { + this.pageNumber = pageNumber; + this.cache = cache; + if (bbox == null) { + bbox = new Rectangle2D.Float(0, 0, 1, 1); + } + rotation = rotation % 360; // less than a full turn + if (rotation < 0) { + rotation += 360; + } + rotation = rotation / 90; // for page rotation use only multiples of 90 degrees + rotation = rotation * 90; // 0, 90, 180, 270 + this.rotation = rotation; + if (rotation == 90 || rotation == 270) { + bbox = new Rectangle2D.Double(bbox.getX(), bbox.getY(), bbox.getHeight(), bbox.getWidth()); + } + this.bbox = bbox; + // initialize the cache of images and parsers + this.renderers = Collections.synchronizedMap(new HashMap>()); + // initialize the list of commands + this.commands = Collections.synchronizedList(new ArrayList(250)); + // corresponding pop in PDFParser -> setStatus + this.addPush(); + } + + /** + * Get the width and height of this image in the correct aspect ratio. + * The image returned will have at least one of the width and + * height values identical to those requested. The other + * dimension may be smaller, so as to keep the aspect ratio + * the same as in the original page. + * + * @param width + * the maximum width of the image + * @param height + * the maximum height of the image + * @param clip + * the region in page space of the page to + * display. It may be null, in which the page's defined crop box + * will be used. + */ + public Dimension getUnstretchedSize(int width, int height, Rectangle2D clip) { + if (clip == null) { + clip = this.bbox; + } else { + if (getRotation() == 90 || getRotation() == 270) { + clip = new Rectangle2D.Double(clip.getX(), clip.getY(), clip.getHeight(), clip.getWidth()); + } + } + double ratio = clip.getHeight() / clip.getWidth(); + double askratio = (double) height / (double) width; + if (askratio > ratio) { + // asked for something too high + height = (int) (width * ratio + 0.5); + } else { + // asked for something too wide + width = (int) (height / ratio + 0.5); + } + return new Dimension(width, height); + } + + /** + * Get an image producer which can be used to draw the image + * represented by this PDFPage. The ImageProducer is guaranteed to + * stay in sync with the PDFPage as commands are added to it. + * + * The image will contain the section of the page specified by the clip, + * scaled to fit in the area given by width and height. + * + * @param width + * the width of the image to be produced + * @param height + * the height of the image to be produced + * @param clip + * the region in page space of the entire page to + * display + * @param observer + * an image observer who will be notified when the + * image changes, or null + * @return an Image that contains the PDF data + */ + public Image getImage(int width, int height, Rectangle2D clip, ImageObserver observer) { + return getImage(width, height, clip, observer, true, false); + } + + /** + * Get an image producer which can be used to draw the image + * represented by this PDFPage. The ImageProducer is guaranteed to + * stay in sync with the PDFPage as commands are added to it. + * + * The image will contain the section of the page specified by the clip, + * scaled to fit in the area given by width and height. + * + * @param width + * the width of the image to be produced + * @param height + * the height of the image to be produced + * @param clip + * the region in page space of the entire page to + * display + * @param observer + * an image observer who will be notified when the + * image changes, or null + * @param drawbg + * if true, put a white background on the image. If not, + * draw no color (alpha 0) for the background. + * @param wait + * if true, do not return until this image is fully rendered. + * @return an Image that contains the PDF data + */ + public Image getImage(int width, int height, Rectangle2D clip, ImageObserver observer, boolean drawbg, boolean wait) { + // see if we already have this image + BufferedImage image = null; + PDFRenderer renderer = null; + ImageInfo info = new ImageInfo(width, height, clip, null); + if (this.cache != null) { + image = this.cache.getImage(this, info); + renderer = this.cache.getImageRenderer(this, info); + } + // not in the cache, so create it + if (image == null) { + if (drawbg) { + info.bgColor = Color.WHITE; + } + image = new RefImage(info.width, info.height, BufferedImage.TYPE_INT_ARGB); + renderer = new PDFRenderer(this, info, image); + if (this.cache != null) { + this.cache.addImage(this, info, image, renderer); + } + this.renderers.put(info, new WeakReference(renderer)); + } + // the renderer may be null if we are getting this image from the + // cache and rendering has completed. + if (renderer != null) { + if (observer != null) { + renderer.addObserver(observer); + } + + if (!renderer.isFinished()) { + renderer.go(wait); + if (renderer.getStatus() == Watchable.ERROR) { + PDFDebugger.debug("Error during reading image!"); + } + } + } + // return the image + return image; + } + + /** + * get the page number used to lookup this page + * + * @return the page number + */ + public int getPageNumber() { + return this.pageNumber; + } + + /** + * get the aspect ratio of the correctly oriented page. + * + * @return the width/height aspect ratio of the page + */ + public float getAspectRatio() { + return getWidth() / getHeight(); + } + + /** + * get the bounding box of the page, before any rotation. + */ + public Rectangle2D getBBox() { + return this.bbox; + } + + /** + * get the width of this page, after rotation + */ + public float getWidth() { + return (float) this.bbox.getWidth(); + } + + /** + * get the height of this page, after rotation + */ + public float getHeight() { + return (float) this.bbox.getHeight(); + } + + /** + * get the rotation of this image + */ + public int getRotation() { + return this.rotation; + } + + /** + * Get the initial transform to map from a specified clip rectangle in + * pdf coordinates to an image of the specfied width and + * height in device coordinates + * + * @param width + * the width of the image + * @param height + * the height of the image + * @param clip + * the desired clip rectangle (in PDF space) or null to use + * the page's bounding box + */ + public AffineTransform getInitialTransform(int width, int height, Rectangle2D clip) { + AffineTransform at = new AffineTransform(); + switch (getRotation()) { + case 0: + at = new AffineTransform(1, 0, 0, -1, 0, height); + break; + case 90: + at = new AffineTransform(0, 1, 1, 0, 0, 0); + break; + case 180: + at = new AffineTransform(-1, 0, 0, 1, width, 0); + break; + case 270: + at = new AffineTransform(0, -1, -1, 0, width, height); + break; + } + double clipW; + double clipH; + if (clip == null) { + clip = getBBox(); + clipW = clip.getWidth(); + clipH = clip.getHeight(); + } else if (getRotation() == 90 || getRotation() == 270) { + int tmp = width; + width = height; + height = tmp; + clipW = clip.getHeight(); + clipH = clip.getWidth(); + } else { + clipW = clip.getWidth(); + clipH = clip.getHeight(); + } + // now scale the image to be the size of the clip + double scaleX = width / clipW; + double scaleY = height / clipH; + at.scale(scaleX, scaleY); + // create a transform that moves the top left corner of the clip region + // (minX, minY) to (0,0) in the image + at.translate(-clip.getMinX(), -clip.getMinY()); + return at; + } + + /** + * get the current number of commands for this page + */ + public int getCommandCount() { + return this.commands.size(); + } + + /** + * get the command at a given index + */ + public PDFCmd getCommand(int index) { + return this.commands.get(index); + } + + /** + * get all the commands in the current page + */ + public List getCommands() { + return this.commands; + } + + /** + * get all the commands in the current page starting at the given index + */ + public List getCommands(int startIndex) { + return getCommands(startIndex, getCommandCount()); + } + + /* + * get the commands in the page within the given start and end indices + */ + public List getCommands(int startIndex, int endIndex) { + return this.commands.subList(startIndex, endIndex); + } + + public PDFCmd findLastCommand(Class cls) { + int index = this.commands.size(); + while (index-- > 0) { + PDFCmd cmd = this.commands.get(index); + if (cmd.getClass().isAssignableFrom(cls)) { + return cmd; + } + } + return null; + } + + /** + * Add a single command to the page list. + */ + public void addCommand(PDFCmd cmd) { + synchronized (this.commands) { + this.commands.add(cmd); + } + // notify any outstanding images + updateImages(); + } + + /** + * add a collection of commands to the page list. This is probably + * invoked as the result of an XObject 'do' command, or through a + * type 3 font. + */ + public void addCommands(PDFPage page) { + addCommands(page, null); + } + + /** + * add a collection of commands to the page list. This is probably + * invoked as the result of an XObject 'do' command, or through a + * type 3 font. + * + * @param page + * the source of other commands. It MUST be finished. + * @param extra + * a transform to perform before adding the commands. + * If null, no extra transform will be added. + */ + public void addCommands(PDFPage page, AffineTransform extra) { + synchronized (this.commands) { + addPush(); + if (extra != null) { + addXform(extra); + } + // addXform(page.getTransform()); + this.commands.addAll(page.getCommands()); + addPop(); + } + // notify any outstanding images + updateImages(); + } + + /** + * Clear all commands off the current page + */ + public void clearCommands() { + synchronized (this.commands) { + this.commands.clear(); + } + // notify any outstanding images + updateImages(); + } + + /** + * get whether parsing for this PDFPage has been completed and all + * commands are in place. + */ + public boolean isFinished() { + return this.finished; + } + + /** + * wait for finish + */ + public synchronized void waitForFinish() throws InterruptedException { + if (!this.finished) { + wait(); + } + } + + /** + * Stop the rendering of a particular image on this page + */ + public void stop(int width, int height, Rectangle2D clip) { + ImageInfo info = new ImageInfo(width, height, clip); + synchronized (this.renderers) { + // find our renderer + WeakReference rendererRef = this.renderers.get(info); + if (rendererRef != null) { + PDFRenderer renderer = (PDFRenderer) rendererRef.get(); + if (renderer != null) { + // stop it + renderer.stop(); + } + } + } + } + + /** + * The entire page is done. This must only be invoked once. All + * observers will be notified. + */ + public synchronized void finish() { + PDFDebugger.debug("Page finished!", 1000); + this.finished = true; + notifyAll(); + // notify any outstanding images + updateImages(); + } + + /** push the graphics state */ + public void addPush() { + addCommand(new PDFPushCmd()); + } + + /** pop the graphics state */ + public void addPop() { + addCommand(new PDFPopCmd()); + } + + /** concatenate a transform to the graphics state */ + public void addXform(AffineTransform at) { + // PDFXformCmd xc= lastXformCmd(); + // xc.at.concatenate(at); + addCommand(new PDFXformCmd(new AffineTransform(at))); + } + + /** + * set the stroke width + * + * @param w + * the width of the stroke + */ + public PDFChangeStrokeCmd addStrokeWidth(float w) { + PDFChangeStrokeCmd sc = new PDFChangeStrokeCmd(); + // if (w == 0) { + // w = 0.1f; + // } + sc.setWidth(w); + addCommand(sc); + return sc; + } + + /** + * set the end cap style + * + * @param capstyle + * the cap style: 0 = BUTT, 1 = ROUND, 2 = SQUARE + */ + public void addEndCap(int capstyle) { + PDFChangeStrokeCmd sc = new PDFChangeStrokeCmd(); + int cap = BasicStroke.CAP_BUTT; + switch (capstyle) { + case 0: + cap = BasicStroke.CAP_BUTT; + break; + case 1: + cap = BasicStroke.CAP_ROUND; + break; + case 2: + cap = BasicStroke.CAP_SQUARE; + break; + } + sc.setEndCap(cap); + addCommand(sc); + } + + /** + * set the line join style + * + * @param joinstyle + * the join style: 0 = MITER, 1 = ROUND, 2 = BEVEL + */ + public void addLineJoin(int joinstyle) { + PDFChangeStrokeCmd sc = new PDFChangeStrokeCmd(); + int join = BasicStroke.JOIN_MITER; + switch (joinstyle) { + case 0: + join = BasicStroke.JOIN_MITER; + break; + case 1: + join = BasicStroke.JOIN_ROUND; + break; + case 2: + join = BasicStroke.JOIN_BEVEL; + break; + } + sc.setLineJoin(join); + addCommand(sc); + } + + /** + * set the miter limit + */ + public void addMiterLimit(float limit) { + PDFChangeStrokeCmd sc = new PDFChangeStrokeCmd(); + sc.setMiterLimit(limit); + addCommand(sc); + } + + /** + * set the dash style + * + * @param dashary + * the array of on-off lengths + * @param phase + * offset of the array at the start of the line drawing + */ + public void addDash(float[] dashary, float phase) { + PDFChangeStrokeCmd sc = new PDFChangeStrokeCmd(); + sc.setDash(dashary, phase); + addCommand(sc); + } + + /** + * set the current path + * + * @param path + * the path + * @param style + * the style: PDFShapeCmd.STROKE, PDFShapeCmd.FILL, + * @param autoAdjustStroke + * PDFShapeCmd.BOTH, PDFShapeCmd.CLIP, or some combination. + */ + public void addPath(GeneralPath path, int style, boolean autoAdjustStroke) { + addCommand(new PDFShapeCmd(path, style, autoAdjustStroke)); + } + + public void addShadeCommand(PDFPaint p, Rectangle2D box) { + addCommand(new PDFShadeCommand(p, box)); + } + + /** + * set the fill paint + */ + public void addFillPaint(PDFPaint p) { + addCommand(new PDFFillPaintCmd(p)); + } + + /** set the stroke paint */ + public void addStrokePaint(PDFPaint p) { + addCommand(new PDFStrokePaintCmd(p)); + } + + /** + * set the fill alpha + */ + public void addFillAlpha(float a) { + addCommand(new PDFFillAlphaCmd(a)); + } + + /** set the stroke alpha */ + public void addStrokeAlpha(float a) { + addCommand(new PDFStrokeAlphaCmd(a)); + } + + /** + * draw an image + * + * @param image + * the image to draw + */ + public void addImage(PDFImage image) { + addCommand(new PDFImageCmd(image)); + } + + /** + * Notify all images we know about that a command has been added + */ + public void updateImages() { + for (Iterator> i = this.renderers.values().iterator(); i.hasNext();) { + WeakReference ref = i.next(); + PDFRenderer renderer = (PDFRenderer) ref.get(); + if (renderer != null) { + if (renderer.getStatus() == Watchable.NEEDS_DATA) { + // there are watchers. Set the state to paused and + // let the watcher decide when to start. + renderer.setStatus(Watchable.PAUSED); + } + } + } + } + + /************************************************************************* + * Get a list of all annotations for this PDF page + * + * @return List + ************************************************************************/ + public List getAnnots() { + return this.annots; + } + + /************************************************************************* + * Get a list of all annotations of the given type for this PDF page + * + * @return List + ************************************************************************/ + public List getAnnots(AnnotationType type) { + List list = new ArrayList(); + if (this.annots != null) { + for (PDFAnnotation annot : this.annots) { + if (annot.getType() == type) { + list.add(annot); + } + } + } + return list; + } + + /************************************************************************* + * Set annotations for this PDF page + * + * @param annots + ************************************************************************/ + public void setAnnots(List annots) { + this.annots = annots; + } + + public void addAnnotations() { + if(this.annots != null) { + for (PDFAnnotation pdfAnnotation : this.annots) { + // add command to the page if needed + this.commands.addAll(pdfAnnotation.getPageCommandsForAnnotation()); + } + } + } + + public static PDFImageCmd createImageCmd(PDFImage image) { + return new PDFImageCmd(image); + } + + public static PDFPushCmd createPushCmd() { + return new PDFPushCmd(); + } + + public static PDFPopCmd createPopCmd() { + return new PDFPopCmd(); + } + + public static PDFXformCmd createXFormCmd(AffineTransform at) { + return new PDFXformCmd(new AffineTransform(at)); + } +} + +/** +* draw an image +*/ +class PDFImageCmd extends PDFCmd { + PDFImage image; + + public PDFImageCmd(PDFImage image) { + this.image = image; + } + + @Override + public Rectangle2D execute(PDFRenderer state) { + return state.drawImage(this.image); + } +} + +/** +* set the fill paint +*/ +class PDFFillPaintCmd extends PDFCmd { + PDFPaint p; + + public PDFFillPaintCmd(PDFPaint p) { + this.p = p; + } + + @Override + public Rectangle2D execute(PDFRenderer state) { + state.setFillPaint(this.p); + return null; + } +} + +/** +* set the stroke paint +*/ +class PDFStrokePaintCmd extends PDFCmd { + PDFPaint p; + + public PDFStrokePaintCmd(PDFPaint p) { + this.p = p; + } + + @Override + public Rectangle2D execute(PDFRenderer state) { + state.setStrokePaint(this.p); + return null; + } +} + +/** +* set the fill paint +*/ +class PDFFillAlphaCmd extends PDFCmd { + float a; + + public PDFFillAlphaCmd(float a) { + this.a = a; + } + + @Override + public Rectangle2D execute(PDFRenderer state) { + state.setFillAlpha(this.a); + return null; + } +} + +/** +* set the stroke paint +*/ +class PDFStrokeAlphaCmd extends PDFCmd { + float a; + + public PDFStrokeAlphaCmd(float a) { + this.a = a; + } + + @Override + public Rectangle2D execute(PDFRenderer state) { + state.setStrokeAlpha(this.a); + return null; + } +} + +/** +* set the shade paint +*/ +class PDFShadeCommand extends PDFCmd { + PDFPaint p; + Rectangle2D box; + + PDFShadeCommand(PDFPaint p, Rectangle2D box) { + this.p = p; + this.box = box; + } + + PDFShadeCommand(PDFPaint p) { + this.p = p; + this.box = null; + } + + @Override + public Rectangle2D execute(PDFRenderer state) { + // TODO: Not sure this is the right way to get the area for the sh cmd + Shape s = box; + Shape clip = state.getImage().getGraphics().getClipBounds(); + if (clip != null) { + s = clip; + } + if (s == null) { + s = state.getImage().getData().getBounds(); + try { + s = state.getLastTransform().createInverse().createTransformedShape(s); + } catch (NoninvertibleTransformException e) { + BaseWatchable.getErrorHandler().publishException(e); + } + } + state.setFillAlpha(1); + state.setFillPaint(p); + return (new PDFShapeCmd(new GeneralPath(s), PDFShapeCmd.FILL, false)).execute(state); + } +} + +/** +* push the graphics state +*/ +class PDFPushCmd extends PDFCmd { + @Override + public Rectangle2D execute(PDFRenderer state) { + state.push(); + return null; + } +} + +/** +* pop the graphics state +*/ +class PDFPopCmd extends PDFCmd { + @Override + public Rectangle2D execute(PDFRenderer state) { + state.pop(); + return null; + } +} + +/** +* concatenate a transform to the graphics state +*/ +class PDFXformCmd extends PDFCmd { + AffineTransform at; + + public PDFXformCmd(AffineTransform at) { + if (at == null) { + throw new RuntimeException("Null transform in PDFXformCmd"); + } + this.at = at; + } + + @Override + public Rectangle2D execute(PDFRenderer state) { + state.transform(this.at); + return null; + } + + public String toString(PDFRenderer state) { + return "PDFXformCmd: " + this.at; + } + + @Override + public String getDetails() { + StringBuffer buf = new StringBuffer(); + buf.append("PDFXformCommand: \n"); + buf.append(this.at.toString()); + return buf.toString(); + } +} + +/** +* change the stroke style +*/ +class PDFChangeStrokeCmd extends PDFCmd { + float w, limit, phase; + int cap, join; + float[] ary; + + public PDFChangeStrokeCmd() { + this.w = PDFRenderer.NOWIDTH; + this.cap = PDFRenderer.NOCAP; + this.join = PDFRenderer.NOJOIN; + this.limit = PDFRenderer.NOLIMIT; + this.ary = PDFRenderer.NODASH; + this.phase = PDFRenderer.NOPHASE; + } + + /** + * set the width of the stroke. Rendering needs to account for a minimum + * stroke width in creating the output. + * + * @param w + * float + */ + public void setWidth(float w) { + this.w = w; + } + + public void setEndCap(int cap) { + this.cap = cap; + } + + public void setLineJoin(int join) { + this.join = join; + } + + public void setMiterLimit(float limit) { + this.limit = limit; + } + + public void setDash(float[] ary, float phase) { + if (ary != null) { + // make sure no pairs start with 0, since having no opaque + // region doesn't make any sense. + for (int i = 0; i < ary.length - 1; i += 2) { + if (ary[i] == 0) { + /* Give a very small value, since 0 messes java up */ + ary[i] = 0.00001f; + break; + } + } + } + this.ary = ary; + this.phase = phase; + } + + @Override + public Rectangle2D execute(PDFRenderer state) { + state.setStrokeParts(this.w, this.cap, this.join, this.limit, this.ary, this.phase); + return null; + } + + public String toString(PDFRenderer state) { + return "STROKE: w=" + this.w + " cap=" + this.cap + " join=" + this.join + " limit=" + this.limit + " ary=" + this.ary + " phase=" + this.phase; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFPaint.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFPaint.java new file mode 100644 index 0000000000..a5eef2d3bf --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFPaint.java @@ -0,0 +1,78 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Paint; +import java.awt.geom.GeneralPath; +import java.awt.geom.Rectangle2D; + +/** + * PDFPaint is some kind of shader that knows how to fill a path. + * At the moment, only a solid color is implemented, but gradients + * and textures should be possible, too. + * @author Mike Wessler + */ +public class PDFPaint { + + private Paint mainPaint; + + /** + * create a new PDFPaint based on a solid color + */ + protected PDFPaint(Paint p) { + this.mainPaint = p; + } + + /** + * get the PDFPaint representing a solid color + */ + public static PDFPaint getColorPaint(Color c) { + return getPaint(c); + } + + /** + * get the PDFPaint representing a generic paint + */ + public static PDFPaint getPaint(Paint p) { + return new PDFPaint(p); + } + + /** + * fill a path with the paint, and record the dirty area. + * @param state the current graphics state + * @param g the graphics into which to draw + * @param s the path to fill + */ + public Rectangle2D fill(PDFRenderer state, Graphics2D g, + GeneralPath s) { + g.setPaint(this.mainPaint); + g.fill(s); + + return s.createTransformedShape(g.getTransform()).getBounds2D(); + } + + /** + * get the primary color associated with this PDFPaint. + */ + public Paint getPaint() { + return this.mainPaint; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFParseException.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFParseException.java new file mode 100644 index 0000000000..c3464fd824 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFParseException.java @@ -0,0 +1,39 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer; + +import java.io.IOException; + +/** + * an exception class for recording parse errors in the PDF file + * @author Mike Wessler + */ +public class PDFParseException extends IOException { + + public PDFParseException(String msg) { + super(msg); + } + + public PDFParseException(String msg, Throwable cause) { + this(msg); + initCause(cause); + } + + +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFParser.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFParser.java new file mode 100644 index 0000000000..e80a294b39 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFParser.java @@ -0,0 +1,1537 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer; +import static java.awt.geom.Path2D.WIND_EVEN_ODD; + +import java.awt.geom.AffineTransform; +import java.awt.geom.GeneralPath; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.nio.ByteBuffer; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Stack; + +import com.github.librepdf.pdfrenderer.colorspace.PDFColorSpace; +import com.github.librepdf.pdfrenderer.colorspace.PatternSpace; +import com.github.librepdf.pdfrenderer.decode.PDFDecoder; +import com.github.librepdf.pdfrenderer.font.PDFFont; +import com.github.librepdf.pdfrenderer.pattern.PDFShader; +import com.github.librepdf.pdfrenderer.PDFDebugger.DebugStopException; + +/** +* PDFParser is the class that parses a PDF content stream and +* produces PDFCmds for a PDFPage. You should never ever see it run: +* it gets created by a PDFPage only if needed, and may even run in +* its own thread. +* +* @author Mike Wessler +*/ +public class PDFParser extends BaseWatchable { + private int mDebugCommandIndex; + // ---- parsing variables + private Stack stack; // stack of Object + private Stack parserStates; // stack of RenderState + // the current render state + private ParserState state; + private GeneralPath path; + private int clip; + private int loc; + private boolean resend = false; + private Tok tok; + private boolean catchexceptions = true; // Indicates state of BX...EX + /** + * a weak reference to the page we render into. For the page + * to remain available, some other code must retain a strong reference to + * it. + */ + private final WeakReference pageRef; + /** + * the actual command, for use within a singe iteration. Note that + * this must be released at the end of each iteration to assure the + * page can be collected if not in use + */ + private PDFPage cmds; + // ---- result variables + byte[] stream; + HashMap resources; + + boolean errorwritten = false; + private boolean autoAdjustStroke = false; + private boolean strokeOverprint; + private int strokeOverprintMode; + private boolean fillOverprint; + private int fillOverprintMode; + private boolean addAnnotation; + + /** + * Don't call this constructor directly. Instead, use + * PDFFile.getPage(int pagenum) to get a PDFPage. There should + * never be any reason for a user to create, access, or hold + * on to a PDFParser. + */ + public PDFParser(PDFPage cmds, byte[] stream, HashMap resources) { + super(); + this.pageRef = new WeakReference(cmds); + this.resources = resources; + if (resources == null) { + this.resources = new HashMap(); + } + this.stream = stream; + } + + // /////////////////////////////////////////////////////////////// + // B E G I N R E A D E R S E C T I O N + // /////////////////////////////////////////////////////////////// + /** + * a token from a PDF Stream + */ + static class Tok { + /** begin bracket < */ + public static final int BRKB = 11; + /** end bracket > */ + public static final int BRKE = 10; + /** begin array [ */ + public static final int ARYB = 9; + /** end array ] */ + public static final int ARYE = 8; + /** String (, readString looks for trailing ) */ + public static final int STR = 7; + /** begin brace { */ + public static final int BRCB = 5; + /** end brace } */ + public static final int BRCE = 4; + /** number */ + public static final int NUM = 3; + /** keyword */ + public static final int CMD = 2; + /** name (begins with /) */ + public static final int NAME = 1; + /** unknown token */ + public static final int UNK = 0; + /** end of stream */ + public static final int EOF = -1; + /** the string value of a STR, NAME, or CMD token */ + public String name; + /** the value of a NUM token */ + public double value; + /** the type of the token */ + public int type; + + /** a printable representation of the token */ + @Override + public String toString() { + if (this.type == NUM) { + return "NUM: " + this.value; + } else if (this.type == CMD) { + return "CMD: " + this.name; + } else if (this.type == UNK) { + return "UNK"; + } else if (this.type == EOF) { + return "EOF"; + } else if (this.type == NAME) { + return "NAME: " + this.name; + } else if (this.type == CMD) { + return "CMD: " + this.name; + } else if (this.type == STR) { + return "STR: (" + this.name; + } else if (this.type == ARYB) { + return "ARY ["; + } else if (this.type == ARYE) { + return "ARY ]"; + } else { + return "some kind of brace (" + this.type + ")"; + } + } + + /** + * reset the token to it's original state + */ + public void reset() { + name = null; + value = 0.0; + type = UNK; + } + } + + /** + * get the next token. + */ + private Tok nextToken() { + if (this.resend) { + this.resend = false; + return this.tok; + } + if (this.tok != null) { + this.tok.reset(); + } else { + tok = new Tok(); + } + // skip whitespace + while (this.loc < this.stream.length && PDFFile.isWhiteSpace(this.stream[this.loc])) { + this.loc++; + } + if (this.loc >= this.stream.length) { + this.tok.type = Tok.EOF; + return this.tok; + } + int c = this.stream[this.loc++]; + // examine the character: + while (c == '%' || c == 28) { + // skip comments + StringBuffer comment = new StringBuffer(); + while (this.loc < this.stream.length && c != '\n') { + comment.append((char) c); + c = this.stream[this.loc++]; + } + if (this.loc < this.stream.length) { + c = this.stream[this.loc++]; // eat the newline + if (c == '\r') { + c = this.stream[this.loc++]; // eat a following return + } + // skip whitespace + while (this.loc < this.stream.length && PDFFile.isWhiteSpace(c)) { + c = this.stream[this.loc++]; + } + } + PDFDebugger.debug("Read comment: " + comment.toString(), -1); + } + if (c == '[') { + this.tok.type = Tok.ARYB; + } else if (c == ']') { + this.tok.type = Tok.ARYE; + } else if (c == '(') { + // read a string + this.tok.type = Tok.STR; + this.tok.name = readString(); + } else if (c == '{') { + this.tok.type = Tok.BRCB; + } else if (c == '}') { + this.tok.type = Tok.BRCE; + } else if (c == '<' && this.stream[this.loc++] == '<') { + this.tok.type = Tok.BRKB; + } else if (c == '>' && this.stream[this.loc++] == '>') { + this.tok.type = Tok.BRKE; + } else if (c == '<') { + this.loc--; + this.tok.type = Tok.STR; + this.tok.name = readByteArray(); + } else if (c == '/') { + this.tok.type = Tok.NAME; + this.tok.name = readName(); + } else if (c == '.' || c == '-' || (c >= '0' && c <= '9')) { + this.loc--; + this.tok.type = Tok.NUM; + this.tok.value = readNum(); + } else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '\'' || c == '"') { + this.loc--; + this.tok.type = Tok.CMD; + this.tok.name = readName(); + } else { + PDFDebugger.debug("Encountered character: " + c + " (" + (char) c + ")", 1); + this.tok.type = Tok.UNK; + } + return this.tok; + } + + /** + * read a name (sequence of non-PDF-delimiting characters) from the + * stream. + */ + private String readName() { + int start = this.loc; + while (this.loc < this.stream.length && PDFFile.isRegularCharacter(this.stream[this.loc])) { + this.loc++; + } + return new String(this.stream, start, this.loc - start); + } + + /** + * read a floating point number from the stream + */ + private double readNum() { + int c = this.stream[this.loc++]; + boolean neg = c == '-'; + boolean sawdot = c == '.'; + double dotmult = sawdot ? 0.1 : 1; + double value = (c >= '0' && c <= '9') ? c - '0' : 0; + while (true) { + c = this.stream[this.loc++]; + if (c == '.') { + if (sawdot) { + this.loc--; + break; + } + sawdot = true; + dotmult = 0.1; + } else if (c >= '0' && c <= '9') { + int val = c - '0'; + if (sawdot) { + value += val * dotmult; + dotmult *= 0.1; + } else { + value = value * 10 + val; + } + } else { + this.loc--; + break; + } + } + if (neg) { + value = -value; + } + return value; + } + + /** + *

+ * read a String from the stream. Strings begin with a '(' character, which has already been + * read, and end with a balanced ')' character. A '\' character starts an escape sequence of up + * to three octal digits. + *

+ * + *

+ * Parenthesis must be enclosed by a balanced set of parenthesis, so a string may enclose + * balanced parenthesis. + *

+ * + * @return the string with escape sequences replaced with their + * values + */ + private String readString() { + int parenLevel = 0; + StringBuffer sb = new StringBuffer(); + while (this.loc < this.stream.length) { + int c = this.stream[this.loc++]; + if (c == ')') { + if (parenLevel-- == 0) { + break; + } + } else if (c == '(') { + parenLevel++; + } else if (c == '\\') { + // escape sequences + c = this.stream[this.loc++]; + if (c >= '0' && c < '8') { + int count = 0; + int val = 0; + while (c >= '0' && c < '8' && count < 3) { + val = val * 8 + c - '0'; + c = this.stream[this.loc++]; + count++; + } + this.loc--; + c = val; + } else if (c == 'n') { + c = '\n'; + } else if (c == 'r') { + c = '\r'; + } else if (c == 't') { + c = '\t'; + } else if (c == 'b') { + c = '\b'; + } else if (c == 'f') { + c = '\f'; + } else if (c == '\n' || c == '\r') { + continue; + } + } + sb.append((char) c); + } + return sb.toString(); + } + + /** + * read a byte array from the stream. Byte arrays begin with a '<' + * character, which has already been read, and end with a '>' + * character. Each byte in the array is made up of two hex characters, + * the first being the high-order bit. + * + * We translate the byte arrays into char arrays by combining two bytes + * into a character, and then translate the character array into a string. + * [JK FIXME this is probably a really bad idea!] + * + * @return the byte array + */ + private String readByteArray() { + StringBuffer buf = new StringBuffer(); + int count = 0; + char w = (char) 0; + // read individual bytes and format into a character array + while ((this.loc < this.stream.length) && (this.stream[this.loc] != '>')) { + char c = (char) this.stream[this.loc]; + byte b = (byte) 0; + if (c >= '0' && c <= '9') { + b = (byte) (c - '0'); + } else if (c >= 'a' && c <= 'f') { + b = (byte) (10 + (c - 'a')); + } else if (c >= 'A' && c <= 'F') { + b = (byte) (10 + (c - 'A')); + } else { + this.loc++; + continue; + } + // calculate where in the current byte this character goes + int offset = 1 - (count % 2); + w |= (0xf & b) << (offset * 4); + // increment to the next char if we've written four bytes + if (offset == 0) { + buf.append(w); + w = (char) 0; + } + count++; + this.loc++; + } + // ignore trailing '>' + this.loc++; + return buf.toString(); + } + + // /////////////////////////////////////////////////////////////// + // B E G I N P A R S E R S E C T I O N + // /////////////////////////////////////////////////////////////// + /** + * Called to prepare for some iterations + */ + @Override + public void setup() { + this.stack = new Stack(); + this.parserStates = new Stack(); + this.state = new ParserState(); + this.path = new GeneralPath(); + this.loc = 0; + this.clip = 0; + // initialize the ParserState + this.state.fillCS = PDFColorSpace.getColorSpace(PDFColorSpace.COLORSPACE_GRAY); + this.state.strokeCS = PDFColorSpace.getColorSpace(PDFColorSpace.COLORSPACE_GRAY); + this.state.textFormat = new PDFTextFormat(); + } + + /** + * parse the stream. commands are added to the PDFPage initialized + * in the constructor as they are encountered. + *

+ * Page numbers in comments refer to the Adobe PDF specification.
+ * commands are listed in PDF spec 32000-1:2008 in Table A.1 + * + * @return

    + *
  • Watchable.RUNNING when there are commands to be processed
  • Watchable.COMPLETED + * when the page is done and all the commands have been processed
  • Watchable.STOPPED + * if the page we are rendering into is no longer available + *
+ */ + @SuppressWarnings("unused") + @Override + public int iterate() throws Exception { + // make sure the page is still available, and create the reference + // to it for use within this iteration + this.cmds = this.pageRef.get(); + if (this.cmds == null) { + PDFDebugger.debug("Page gone. Stopping", 10); + return STOPPED; + } + Object obj; + try { + obj = parseObject(); + } catch (DebugStopException e) { + return STOPPED; + } + // if there's nothing left to parse, we're done + if (obj == null) { + return COMPLETED; + } + if (obj instanceof Tok) { + // it's a command. figure out what to do. + // (if not, the token will be "pushed" onto the stack) + String cmd = ((Tok) obj).name; + PDFDebugger.debug("Command: " + cmd + " (stack size is " + this.stack.size() + ")", 10); + if (cmd.equals("q")) { + // push the parser state + this.parserStates.push((ParserState) this.state.clone()); + // push graphics state + this.cmds.addPush(); + } else if (cmd.equals("Q")) { + processQCmd(); + } else if (cmd.equals("cm")) { + // set transform to array of values + float[] elts = popFloat(6); + AffineTransform xform = new AffineTransform(elts); + this.cmds.addXform(xform); + } else if (cmd.equals("w")) { + // set stroke width + this.cmds.addStrokeWidth(popFloat()); + } else if (cmd.equals("J")) { + // set end cap style + this.cmds.addEndCap(popInt()); + } else if (cmd.equals("j")) { + // set line join style + this.cmds.addLineJoin(popInt()); + } else if (cmd.equals("M")) { + // set miter limit + this.cmds.addMiterLimit(popInt()); + } else if (cmd.equals("d")) { + // set dash style and phase + float phase = popFloat(); + float[] dashary = popFloatArray(); + if (!PDFDebugger.DISABLE_PATH_STROKE) { + this.cmds.addDash(dashary, phase); + } + } else if (cmd.equals("ri")) { + popString(); + // TODO: do something with rendering intent (page 197) + } else if (cmd.equals("i")) { + popFloat(); + // TODO: do something with flatness tolerance + } else if (cmd.equals("gs")) { + // set graphics state to values in a named dictionary + String popString = popString(); + PDFDebugger.debug("Set GS state "+popString, 10); + setGSState(popString); + } else if (cmd.equals("m")) { + // path move to + float y = popFloat(); + float x = popFloat(); + this.path.moveTo(x, y); + PDFDebugger.logPath(path, "2 moved to " + x + ", " + y); + } else if (cmd.equals("l")) { + // path line to + float y = popFloat(); + float x = popFloat(); + this.path.lineTo(x, y); + PDFDebugger.logPath(path, "1 line to " + x + ", " + y); + } else if (cmd.equals("c")) { + // path curve to + float a[] = popFloat(6); + this.path.curveTo(a[0], a[1], a[2], a[3], a[4], a[5]); + PDFDebugger.logPath(path, "1 curve to " + Arrays.toString(a)); + } else if (cmd.equals("v")) { + // path curve; first control point= start + float a[] = popFloat(4); + Point2D cp = this.path.getCurrentPoint(); + this.path.curveTo((float) cp.getX(), (float) cp.getY(), a[0], a[1], a[2], a[3]); + PDFDebugger.logPath(path, "2 curve to " + Arrays.toString(a) + ", " + cp.getX() + "," + cp.getY()); + } else if (cmd.equals("y")) { + // path curve; last control point= end + float a[] = popFloat(4); + this.path.curveTo(a[0], a[1], a[2], a[3], a[2], a[3]); + PDFDebugger.logPath(path, "3 curve to " + Arrays.toString(a)); + } else if (cmd.equals("h")) { + tryClosingPath(); + PDFDebugger.logPath(path, "closed"); + } else if (cmd.equals("re")) { + // path add rectangle + float a[] = popFloat(4); + this.path.moveTo(a[0], a[1]); + PDFDebugger.logPath(path, "1 moved to " + a[0] + "," + a[1]); + this.path.lineTo(a[0] + a[2], a[1]); + PDFDebugger.logPath(path, "2 line to " + (a[0] + a[2]) + "," + a[1]); + this.path.lineTo(a[0] + a[2], a[1] + a[3]); + PDFDebugger.logPath(path, "3 line to " + (a[0] + a[2]) + "," + (a[1] + a[3])); + this.path.lineTo(a[0], a[1] + a[3]); + PDFDebugger.logPath(path, "4 line to " + a[0] + "," + (a[1] + a[3])); + tryClosingPath(); + PDFDebugger.logPath(path, "closed"); + } else if (cmd.equals("S")) { + // stroke the path + if (!PDFDebugger.DISABLE_PATH_STROKE || (!PDFDebugger.DISABLE_CLIP && this.clip == PDFShapeCmd.CLIP)) { + if(autoAdjustStroke || strokeOverprint || fillOverprint) { + path.closePath(); + PDFDebugger.logPath(path, "closed"); + } + this.cmds.addPath(this.path, PDFShapeCmd.STROKE | this.clip, this.autoAdjustStroke); + } + this.clip = 0; + this.path = new GeneralPath(); + PDFDebugger.logPath(path, "new path"); + } else if (cmd.equals("s")) { + tryClosingPath(); + PDFDebugger.logPath(path, "closed"); + if (!PDFDebugger.DISABLE_PATH_STROKE || (!PDFDebugger.DISABLE_CLIP && this.clip == PDFShapeCmd.CLIP)) { + this.cmds.addPath(this.path, PDFShapeCmd.STROKE | this.clip, this.autoAdjustStroke); + } + this.clip = 0; + this.path = new GeneralPath(); + PDFDebugger.logPath(path, "new path"); + } else if (cmd.equals("f") || cmd.equals("F")) { + tryClosingPath(); + // fill the path (close/not close identical) + if (!PDFDebugger.DISABLE_PATH_FILL || (!PDFDebugger.DISABLE_CLIP && this.clip == PDFShapeCmd.CLIP)) { + this.cmds.addPath(this.path, PDFShapeCmd.FILL | this.clip, this.autoAdjustStroke); + } + this.clip = 0; + this.path = new GeneralPath(); + PDFDebugger.logPath(path, "new path"); + } else if (cmd.equals("f*")) { + // fill the path using even/odd rule + this.path.setWindingRule(WIND_EVEN_ODD); + PDFDebugger.logPath(path, "set winding rule" + WIND_EVEN_ODD); + if (!PDFDebugger.DISABLE_PATH_FILL || (!PDFDebugger.DISABLE_CLIP && this.clip == PDFShapeCmd.CLIP)) { + this.cmds.addPath(this.path, PDFShapeCmd.FILL | this.clip, this.autoAdjustStroke); + } + this.clip = 0; + this.path = new GeneralPath(); + PDFDebugger.logPath(path, "new path"); + } else if (cmd.equals("B")) { + // fill and stroke the path + if (!PDFDebugger.DISABLE_PATH_STROKE_FILL || (!PDFDebugger.DISABLE_CLIP && this.clip == PDFShapeCmd.CLIP)) { + this.cmds.addPath(this.path, PDFShapeCmd.BOTH | this.clip, this.autoAdjustStroke); + } + this.clip = 0; + this.path = new GeneralPath(); + PDFDebugger.logPath(path, "new path"); + } else if (cmd.equals("B*")) { + // fill path using even/odd rule and stroke it + this.path.setWindingRule(WIND_EVEN_ODD); + PDFDebugger.logPath(path, "set winding rule" + WIND_EVEN_ODD); + if (!PDFDebugger.DISABLE_PATH_STROKE_FILL || (!PDFDebugger.DISABLE_CLIP && this.clip == PDFShapeCmd.CLIP)) { + this.cmds.addPath(this.path, PDFShapeCmd.BOTH | this.clip, this.autoAdjustStroke); + } + this.clip = 0; + this.path = new GeneralPath(); + PDFDebugger.logPath(path, "new path"); + } else if (cmd.equals("b")) { + tryClosingPath(); + PDFDebugger.logPath(path, "close"); + if (!PDFDebugger.DISABLE_PATH_STROKE_FILL || (!PDFDebugger.DISABLE_CLIP && this.clip == PDFShapeCmd.CLIP)) { + this.cmds.addPath(this.path, PDFShapeCmd.BOTH | this.clip, this.autoAdjustStroke); + } + this.clip = 0; + this.path = new GeneralPath(); + PDFDebugger.logPath(path, "new path"); + } else if (cmd.equals("b*")) { + tryClosingPath(); + PDFDebugger.logPath(path, "close"); + this.path.setWindingRule(WIND_EVEN_ODD); + PDFDebugger.logPath(path, "set winding rule " + WIND_EVEN_ODD); + if (!PDFDebugger.DISABLE_PATH_STROKE_FILL || (!PDFDebugger.DISABLE_CLIP && this.clip == PDFShapeCmd.CLIP)) { + this.cmds.addPath(this.path, PDFShapeCmd.BOTH | this.clip, this.autoAdjustStroke); + } + this.clip = 0; + this.path = new GeneralPath(); + PDFDebugger.logPath(path, "new path"); + } else if (cmd.equals("n")) { + if (path.getCurrentPoint() != null) { + tryClosingPath(); + PDFDebugger.logPath(path, "closed"); + } + // clip with the path and discard it + if (!PDFDebugger.DISABLE_CLIP) { + if (this.clip != 0) { + this.cmds.addPath(this.path, this.clip, this.autoAdjustStroke); + } + } + this.clip = 0; + this.path = new GeneralPath(); + PDFDebugger.logPath(path, "new path"); + } else if (cmd.equals("W")) { + // mark this path for clipping! + this.clip = PDFShapeCmd.CLIP; + } else if (cmd.equals("W*")) { + // mark this path using even/odd rule for clipping + this.path.setWindingRule(WIND_EVEN_ODD); + PDFDebugger.logPath(path, "set winding rule " + WIND_EVEN_ODD); + this.clip = PDFShapeCmd.CLIP; + } else if (cmd.equals("sh")) { + // shade a region that is defined by the shader itself. + // shading the current space from a dictionary + // should only be used for limited-dimension shadings + String gdictname = popString(); + // set up the pen to do a gradient fill according + // to the dictionary + PDFObject shobj = findResource(gdictname, "Shading"); + if (!PDFDebugger.DISABLE_SHADER) { + doShader(shobj); + } + } else if (cmd.equals("CS")) { + // set the stroke color space + this.state.strokeCS = parseColorSpace(new PDFObject(this.stack.pop())); + } else if (cmd.equals("cs")) { + // set the fill color space + this.state.fillCS = parseColorSpace(new PDFObject(this.stack.pop())); + } else if (cmd.equals("SC")) { + // set the stroke color + int n = this.state.strokeCS.getNumComponents(); + this.cmds.addStrokePaint(this.state.strokeCS.getPaint(popFloat(n))); + } else if (cmd.equals("SCN")) { + // set the stroke colour + if (this.state.strokeCS instanceof PatternSpace) { + this.cmds.addFillPaint(doPattern((PatternSpace) this.state.strokeCS)); + } else { + int n = this.state.strokeCS.getNumComponents(); + this.cmds.addStrokePaint(this.state.strokeCS.getPaint(popFloat(n))); + } + } else if (cmd.equals("sc")) { + // set the fill color + int n = this.state.fillCS.getNumComponents(); + this.cmds.addFillPaint(this.state.fillCS.getPaint(popFloat(n))); + } else if (cmd.equals("scn")) { + if (this.state.fillCS instanceof PatternSpace) { + this.cmds.addFillPaint(doPattern((PatternSpace) this.state.fillCS)); + } else { + int n = this.state.fillCS.getNumComponents(); + this.cmds.addFillPaint(this.state.fillCS.getPaint(popFloat(n))); + } + } else if (cmd.equals("G")) { + // set the stroke color to a Gray value + this.state.strokeCS = PDFColorSpace.getColorSpace(PDFColorSpace.COLORSPACE_GRAY); + this.cmds.addStrokePaint(this.state.strokeCS.getPaint(popFloat(1))); + } else if (cmd.equals("g")) { + // set the fill color to a Gray value + this.state.fillCS = PDFColorSpace.getColorSpace(PDFColorSpace.COLORSPACE_GRAY); + this.cmds.addFillPaint(this.state.fillCS.getPaint(popFloat(1))); + } else if (cmd.equals("RG")) { + // set the stroke color to an RGB value + this.state.strokeCS = PDFColorSpace.getColorSpace(PDFColorSpace.COLORSPACE_RGB); + this.cmds.addStrokePaint(this.state.strokeCS.getPaint(popFloat(3))); + } else if (cmd.equals("rg")) { + // set the fill color to an RGB value + this.state.fillCS = PDFColorSpace.getColorSpace(PDFColorSpace.COLORSPACE_RGB); + this.cmds.addFillPaint(this.state.fillCS.getPaint(popFloat(3))); + } else if (cmd.equals("K")) { +// if(strokeOverprint && strokeOverprintMode == 1) { +// if (this.state.strokeCS instanceof PatternSpace) { +// this.cmds.addFillPaint(doPattern((PatternSpace) this.state.strokeCS)); +// } else { +// int n = this.state.strokeCS.getNumComponents(); +// this.cmds.addStrokePaint(this.state.strokeCS.getPaint(popFloat(n))); +// } +// }else { + // set the stroke color to a CMYK value + this.state.strokeCS = PDFColorSpace.getColorSpace(PDFColorSpace.COLORSPACE_CMYK); + this.cmds.addStrokePaint(this.state.strokeCS.getPaint(popFloat(4))); +// } + } else if (cmd.equals("k")) { +// if(fillOverprint && fillOverprintMode == 1) { +// // if OP = true and OPM = 1 apply the same as in "scn" +// if (this.state.fillCS instanceof PatternSpace) { +// this.cmds.addFillPaint(doPattern((PatternSpace) this.state.fillCS)); +// } else { +// // set the fill color to a CMYK value +// int n = this.state.fillCS.getNumComponents(); +// this.cmds.addFillPaint(this.state.fillCS.getPaint(popFloat(n))); +// } +// }else { + this.state.fillCS = PDFColorSpace.getColorSpace(PDFColorSpace.COLORSPACE_CMYK); + this.cmds.addFillPaint(this.state.fillCS.getPaint(popFloat(4))); +// } + } else if (cmd.equals("Do")) { + // make a do call on the referenced object + String name = popString(); + if (PDFDebugger.DEBUG_IMAGES) { + PDFDebugger.debug("XObject reference to " + name); + } + PDFObject xobj = findResource(name, "XObject"); + doXObject(xobj); + } else if (cmd.equals("BT")) { + processBTCmd(); + } else if (cmd.equals("ET")) { + // end of text. noop + this.state.textFormat.end(); + } else if (cmd.equals("Tc")) { + // set character spacing + this.state.textFormat.setCharSpacing(popFloat()); + } else if (cmd.equals("Tw")) { + // set word spacing + this.state.textFormat.setWordSpacing(popFloat()); + } else if (cmd.equals("Tz")) { + // set horizontal scaling + this.state.textFormat.setHorizontalScale(popFloat()); + } else if (cmd.equals("TL")) { + // set leading + this.state.textFormat.setLeading(popFloat()); + } else if (cmd.equals("Tf")) { + // set text font + float sz = popFloat(); + String fontref = popString(); + this.state.textFormat.setFont(getFontFrom(fontref), sz); + } else if (cmd.equals("Tr")) { + // set text rendering mode + this.state.textFormat.setMode(popInt()); + } else if (cmd.equals("Ts")) { + // set text rise + this.state.textFormat.setRise(popFloat()); + } else if (cmd.equals("Td")) { + // set text matrix location + float y = popFloat(); + float x = popFloat(); + this.state.textFormat.carriageReturn(x, y); + } else if (cmd.equals("TD")) { + // set leading and matrix: -y TL x y Td + float y = popFloat(); + float x = popFloat(); + this.state.textFormat.setLeading(-y); + this.state.textFormat.carriageReturn(x, y); + } else if (cmd.equals("Tm")) { + // set text matrix + this.state.textFormat.setMatrix(popFloat(6)); + } else if (cmd.equals("T*")) { + // go to next line + this.state.textFormat.carriageReturn(); + } else if (cmd.equals("Tj")) { + // show text + this.state.textFormat.doText(this.cmds, popString(), this.autoAdjustStroke); + } else if (cmd.equals("\'")) { + // next line and show text: T* string Tj + this.state.textFormat.carriageReturn(); + this.state.textFormat.doText(this.cmds, popString(), this.autoAdjustStroke); + } else if (cmd.equals("\"")) { + // draw string on new line with char & word spacing: + // aw Tw ac Tc string ' + String string = popString(); + float ac = popFloat(); + float aw = popFloat(); + this.state.textFormat.setWordSpacing(aw); + this.state.textFormat.setCharSpacing(ac); + this.state.textFormat.doText(this.cmds, string, this.autoAdjustStroke); + } else if (cmd.equals("TJ")) { + // show kerned string + this.state.textFormat.doText(this.cmds, popArray(), this.autoAdjustStroke); + } else if (cmd.equals("BI")) { + // parse inline image + parseInlineImage(); + } else if (cmd.equals("BX")) { + this.catchexceptions = true; // ignore errors + } else if (cmd.equals("EX")) { + this.catchexceptions = false; // stop ignoring errors + } else if (cmd.equals("MP")) { + // mark point (role= mark role name) + popString(); + } else if (cmd.equals("DP")) { + // mark point with dictionary (role, ref) + // result is either inline dict or name in "Properties" rsrc + this.stack.pop(); + popString(); + } else if (cmd.equals("BMC")) { + // begin marked content (role) + popString(); + } else if (cmd.equals("BDC")) { + // begin marked content with dict (role, ref) + // result is either inline dict or name in "Properties" rsrc + this.stack.pop(); + popString(); + } else if (cmd.equals("EMC")) { + // end marked content + } else if (cmd.equals("d0")) { + // character width in type3 fonts + popFloat(2); + } else if (cmd.equals("d1")) { + // character width in type3 fonts + popFloat(6); + } else if (cmd.equals("QBT")) {// 'Q' & 'BT' mushed together! + processQCmd(); + processBTCmd(); + } else if (cmd.equals("Qq")) {// 'Q' & 'q' mushed together! + processQCmd(); + // push the parser state + this.parserStates.push((ParserState) this.state.clone()); + // push graphics state + this.cmds.addPush(); + } else if (cmd.equals("qBT")) {// 'q' & 'BT' mushed together! + // push the parser state + this.parserStates.push((ParserState) this.state.clone()); + // push graphics state + this.cmds.addPush(); + processBTCmd(); + } else if (cmd.equals("q1")) { + PDFDebugger.debug("**** WARNING: Not handled command: " + cmd + " **************************", 10); + } else if (cmd.equals("q0")) { + PDFDebugger.debug("**** WARNING: Not handled command: " + cmd + " **************************", 10); + } else { + if (this.catchexceptions) { + PDFDebugger.debug("**** WARNING: Unknown command: " + cmd + " **************************", 10); + } else { + throw new PDFParseException("Unknown command: " + cmd); + } + } + if (this.stack.size() != 0) { + PDFDebugger.debug("**** WARNING! Stack not zero! (cmd=" + cmd + ", size=" + this.stack.size() + ") *************************", 10); + this.stack.setSize(0); + } + } else { + this.stack.push(obj); + } + // release or reference to the page object, so that it can be + // gc'd if it is no longer in use + this.cmds = null; + return RUNNING; + } + + /** + * Try to close a path but don't fail with exception if this is not working. + * This is just a workaround for some PDFs with wrong content... + */ + private void tryClosingPath() { + try { + this.path.closePath(); + PDFDebugger.logPath(path, "closed"); + }catch(java.awt.geom.IllegalPathStateException e) { + PDFDebugger.debug("Failed to close path", 1000); + } + } + + @SuppressWarnings("unused") + private void onNextObject(Tok obj) throws DebugStopException { + String progress; + if (true) { + double percent = (100d * this.loc) / this.stream.length; + NumberFormat nf = NumberFormat.getInstance(); + nf.setMinimumFractionDigits(1); + nf.setMaximumFractionDigits(1); + progress = nf.format(percent) + "%"; + } else { + progress = this.loc + " of " + this.stream.length; + } + String operators = ""; + for (Object operator : this.stack) { + operators += operator + " "; + } + if (PDFDebugger.DEBUG_OPERATORS) { + PDFDebugger.debug("parser{" + hashCode() + "} " + progress + ": #" + mDebugCommandIndex + " \t" + operators + obj.name); + } + mDebugCommandIndex++; + if (PDFDebugger.DEBUG_STOP_AT_INDEX > 0 && mDebugCommandIndex > PDFDebugger.DEBUG_STOP_AT_INDEX) { + System.err.println("Debugging: stopped at instruction #" + mDebugCommandIndex); + throw new DebugStopException(); + } + if (PDFDebugger.DRAW_DELAY > 0) { + try { + Thread.sleep(PDFDebugger.DRAW_DELAY); + } catch (InterruptedException e) { + } + } + } + + /** + * abstracted command processing for Q command. Used directly and as + * part of processing of mushed QBT command. + */ + private void processQCmd() { + // pop graphics state ('Q') + this.cmds.addPop(); + // pop the parser state + if (this.parserStates.isEmpty() == false) { + this.state = this.parserStates.pop(); + } + } + + /** + * abstracted command processing for BT command. Used directly and as + * part of processing of mushed QBT command. + */ + private void processBTCmd() { + // begin text block: reset everything. + this.state.textFormat.reset(); + } + + /** + * Cleanup when iteration is done + */ + @Override + public void cleanup() { + this.state.textFormat.flush(); + this.cmds.finish(); + this.stack = null; + this.parserStates = null; + this.state = null; + this.path = null; + this.cmds = null; + this.tok = null; + } + + public void dumpStreamToError() { + if (this.errorwritten) { + return; + } + this.errorwritten = true; + try { + File oops = File.createTempFile("PDFError", ".err"); + FileOutputStream fos = new FileOutputStream(oops); + fos.write(this.stream); + fos.close(); + } catch (IOException ioe) { /* Do nothing */ + } + ; + } + + // /////////////////////////////////////////////////////////////// + // H E L P E R S + // /////////////////////////////////////////////////////////////// + /** + * get a property from a named dictionary in the resources of this + * content stream. + * + * @param name + * the name of the property in the dictionary + * @param inDict + * the name of the dictionary in the resources + * @return the value of the property in the dictionary + */ + private PDFObject findResource(String name, String inDict) throws IOException { + if (inDict != null) { + PDFObject in = this.resources.get(inDict); + if (in == null || in.getType() != PDFObject.DICTIONARY) { + throw new PDFParseException("No dictionary called " + inDict + " found in the resources"); + } + return in.getDictRef(name); + } else { + return this.resources.get(name); + } + } + + /** + * Insert a PDF object into the command stream. The object must + * either be an Image or a Form, which is a set of PDF commands + * in a stream. + * + * @param obj + * the object to insert, an Image or a Form. + */ + private void doXObject(PDFObject obj) throws IOException { + String type = obj.getDictRef("Subtype").getStringValue(); + if (type == null) { + type = obj.getDictRef("S").getStringValue(); + } + if (type.equals("Image")) { + doImage(obj); + } else if (type.equals("Form")) { + doForm(obj); + } else { + throw new PDFParseException("Unknown XObject subtype: " + type); + } + } + + /** + * Parse image data into a Java BufferedImage and add the image + * command to the page. + * + * @param obj + * contains the image data, and a dictionary describing + * the width, height and color space of the image. + */ + private void doImage(PDFObject obj) throws IOException { + if (!PDFDebugger.DISABLE_IMAGES) { + if (PDFDebugger.DEBUG_IMAGES) { + final boolean jpegDecode = PDFDecoder.isLastFilter(obj, PDFDecoder.DCT_FILTERS); + if (jpegDecode) { + PDFDebugger.debug("Image is JPEG"); + } else { + PDFDebugger.debug("Image not JPEG"); + } + } + this.cmds.addImage(PDFImage.createImage(obj, this.resources, false)); + } + } + + /** + * Inject a stream of PDF commands onto the page. Optimized to cache + * a parsed stream of commands, so that each Form object only needs + * to be parsed once. + * + * @param obj + * a stream containing the PDF commands, a transformation + * matrix, bounding box, and resources. + */ + private void doForm(PDFObject obj) throws IOException { + // check to see if we've already parsed this sucker + PDFPage formCmds = (PDFPage) obj.getCache(); + if (formCmds == null) { + // rats. parse it. + AffineTransform at; + Rectangle2D bbox; + PDFObject matrix = obj.getDictRef("Matrix"); + if (matrix == null) { + at = new AffineTransform(); + } else { + float elts[] = new float[6]; + for (int i = 0; i < elts.length; i++) { + elts[i] = (matrix.getAt(i)).getFloatValue(); + } + at = new AffineTransform(elts); + } + PDFObject bobj = obj.getDictRef("BBox"); + bbox = new Rectangle2D.Float(bobj.getAt(0).getFloatValue(), bobj.getAt(1).getFloatValue(), bobj.getAt(2).getFloatValue(), bobj.getAt(3).getFloatValue()); + formCmds = new PDFPage(bbox, 0); + formCmds.addXform(at); + HashMap r = new HashMap(this.resources); + PDFObject rsrc = obj.getDictRef("Resources"); + if (rsrc != null) { + r.putAll(rsrc.getDictionary()); + } + PDFParser form = new PDFParser(formCmds, obj.getStream(), r); + form.go(true); + obj.setCache(formCmds); + } + if (!PDFDebugger.DISABLE_FORMS) { + this.cmds.addPush(); + this.cmds.addCommands(formCmds); + this.cmds.addPop(); + } + } + + /** + * Set the values into a PatternSpace + */ + private PDFPaint doPattern(PatternSpace patternSpace) throws IOException { + float[] components = null; + String patternName = popString(); + PDFObject pattern = findResource(patternName, "Pattern"); + if (pattern == null) { + throw new PDFParseException("Unknown pattern : " + patternName); + } + if (this.stack.size() > 0) { + components = popFloat(this.stack.size()); + } + return patternSpace.getPaint(pattern, components, this.resources); + } + + /** + * Parse the next object out of the PDF stream. This could be a + * Double, a String, a HashMap (dictionary), Object[] array, or + * a Tok containing a PDF command. + */ + private Object parseObject() throws PDFParseException, DebugStopException { + Tok t = nextToken(); + if (t.type == Tok.NUM) { + return Double.valueOf(this.tok.value); + } else if (t.type == Tok.STR) { + return this.tok.name; + } else if (t.type == Tok.NAME) { + return this.tok.name; + } else if (t.type == Tok.BRKB) { + HashMap hm = new HashMap(); + String name = null; + Object obj; + while ((obj = parseObject()) != null) { + if (name == null) { + name = (String) obj; + } else { + hm.put(name, new PDFObject(obj)); + name = null; + } + } + if (this.tok.type != Tok.BRKE) { + throw new PDFParseException("Inline dict should have ended with '>>'"); + } + return hm; + } else if (t.type == Tok.ARYB) { + // build an array + ArrayList ary = new ArrayList(); + Object obj; + while ((obj = parseObject()) != null) { + ary.add(obj); + } + if (this.tok.type != Tok.ARYE) { + throw new PDFParseException("Expected ']'"); + } + return ary.toArray(); + } else if (t.type == Tok.CMD) { + onNextObject(t); + return t; + } + PDFDebugger.debug("**** WARNING! parseObject unknown token! (t.type=" + t.type + ") *************************", 10); + return null; + } + + /** + * Parse an inline image. An inline image starts with BI (already + * read, contains a dictionary until ID, and then image data until + * EI. + */ + private void parseInlineImage() throws IOException, DebugStopException { + // build dictionary until ID, then read image until EI + HashMap hm = new HashMap(); + while (true) { + Tok t = nextToken(); + if (t.type == Tok.CMD) { + onNextObject(t); + if (t.name.equals("ID")) { + break; + } + } + // it should be a name; + String name = t.name; + if(PDFDebugger.DEBUG_IMAGES) { + PDFDebugger.debug("ParseInlineImage, token: " + name); + } + if (name.equals("BPC")) { + name = "BitsPerComponent"; + } else if (name.equals("CS")) { + name = "ColorSpace"; + } else if (name.equals("D")) { + name = "Decode"; + } else if (name.equals("DP")) { + name = "DecodeParms"; + } else if (name.equals("F")) { + name = "Filter"; + } else if (name.equals("H")) { + name = "Height"; + } else if (name.equals("IM")) { + name = "ImageMask"; + } else if (name.equals("W")) { + name = "Width"; + } else if (name.equals("I")) { + name = "Interpolate"; + } + Object vobj = parseObject(); + hm.put(name, new PDFObject(vobj)); + } + if (this.stream[this.loc] == '\r') { + this.loc++; + } + if (this.stream[this.loc] == '\n' || this.stream[this.loc] == ' ') { + this.loc++; + } + PDFObject imObj = hm.get("ImageMask"); + if (imObj != null && imObj.getBooleanValue()) { + // [PATCHED by michal.busta@gmail.com] - default value according to PDF spec. is [0, 1] + // there is no need to swap array - PDF image should handle this values + Double[] decode = { Double.valueOf(0), Double.valueOf(1) }; + PDFObject decodeObj = hm.get("Decode"); + if (decodeObj != null) { + decode[0] = Double.valueOf(decodeObj.getAt(0).getDoubleValue()); + decode[1] = Double.valueOf(decodeObj.getAt(1).getDoubleValue()); + } + hm.put("Decode", new PDFObject(decode)); + } + PDFObject obj = new PDFObject(null, PDFObject.DICTIONARY, hm); + int dstart = this.loc; + // now skip data until a whitespace followed by EI + while (!PDFFile.isWhiteSpace(this.stream[this.loc]) || this.stream[this.loc + 1] != 'E' || this.stream[this.loc + 2] != 'I') { + this.loc++; + } + // data runs from dstart to loc + if (PDFDebugger.DEBUG_IMAGES) { + PDFDebugger.debug("InlineImage from " + dstart + " to " + this.loc); + } + byte[] data = new byte[this.loc - dstart]; + System.arraycopy(this.stream, dstart, data, 0, this.loc - dstart); + obj.setStream(ByteBuffer.wrap(data)); + this.loc += 3; + doImage(obj); + } + + /** + * build a shader from a dictionary. + */ + private void doShader(PDFObject shaderObj) throws IOException { + PDFShader shader = PDFShader.getShader(shaderObj, this.resources); + if(shader == null) { + return; + } + this.cmds.addPush(); + Rectangle2D bbox = shader.getBBox(); + if (bbox != null) { + this.cmds.addFillPaint(shader.getPaint()); + this.cmds.addPath(new GeneralPath(bbox), PDFShapeCmd.FILL, this.autoAdjustStroke); + } else { + this.cmds.addFillPaint(shader.getPaint()); + this.cmds.addPath(null, PDFShapeCmd.FILL, this.autoAdjustStroke); + } + this.cmds.addPop(); + } + + /** + * get a PDFFont from the resources, given the resource name of the + * font. + * + * @param fontref + * the resource key for the font + */ + private PDFFont getFontFrom(String fontref) throws IOException { + PDFObject obj = findResource(fontref, "Font"); + return PDFFont.getFont(obj, this.resources); + } + + /** + * add graphics state commands contained within a dictionary. + * + * @param name + * the resource name of the graphics state dictionary + */ + private void setGSState(String name) throws IOException { + // obj must be a string that is a key to the "ExtGState" dict + PDFObject gsobj = findResource(name, "ExtGState"); + // TODO: lots of graphic states are not yet considered, see chapter 8.4.5 of the PDF specification. + // get LW, LC, LJ, Font, SM, CA, ML, D, RI, FL, BM, ca + // out of the reference, which is a dictionary + if (gsobj == null) { + return; + } + PDFObject d; + boolean handled = false; + if ((d = gsobj.getDictRef("LW")) != null) { + this.cmds.addStrokeWidth(d.getFloatValue()); + handled = true; + } + if ((d = gsobj.getDictRef("LC")) != null) { + this.cmds.addEndCap(d.getIntValue()); + handled = true; + } + if ((d = gsobj.getDictRef("LJ")) != null) { + this.cmds.addLineJoin(d.getIntValue()); + handled = true; + } + if ((d = gsobj.getDictRef("Font")) != null) { + this.state.textFormat.setFont(getFontFrom(d.getAt(0).getStringValue()), d.getAt(1).getFloatValue()); + handled = true; + } + if ((d = gsobj.getDictRef("ML")) != null) { + this.cmds.addMiterLimit(d.getFloatValue()); + handled = true; + } + if ((d = gsobj.getDictRef("D")) != null) { + PDFObject pdash[] = d.getAt(0).getArray(); + float dash[] = new float[pdash.length]; + for (int i = 0; i < pdash.length; i++) { + dash[i] = pdash[i].getFloatValue(); + } + if (!PDFDebugger.DISABLE_PATH_STROKE) { + this.cmds.addDash(dash, d.getAt(1).getFloatValue()); + } + handled = true; + } + if ((d = gsobj.getDictRef("CA")) != null) { + this.cmds.addStrokeAlpha(d.getFloatValue()); + handled = true; + } + if ((d = gsobj.getDictRef("ca")) != null) { + this.cmds.addFillAlpha(d.getFloatValue()); + handled = true; + } + if((d = gsobj.getDictRef("SA")) != null) { + // automatic stroke adjustment + this.autoAdjustStroke = d.getBooleanValue(); + handled = true; + } + if((d = gsobj.getDictRef("OP")) != null) { + this.strokeOverprint = d.getBooleanValue(); + PDFObject x = gsobj.getDictRef("OPM"); + if(x!= null) { + this.strokeOverprintMode = x.getIntValue(); + } + handled = true; + } + if((d = gsobj.getDictRef("op")) != null) { + this.fillOverprint = d.getBooleanValue(); + PDFObject x = gsobj.getDictRef("OPM"); + if(x!= null) { + this.fillOverprintMode = x.getIntValue(); + } + handled = true; + } + if(!handled) { + PDFDebugger.debug("graphic state command unknown!", 10); + } + } + + /** + * generate a PDFColorSpace description based on a PDFObject. The + * object could be a standard name, or the name of a resource in + * the ColorSpace dictionary, or a color space name with a defining + * dictionary or stream. + */ + private PDFColorSpace parseColorSpace(PDFObject csobj) throws IOException { + if (csobj == null) { + return this.state.fillCS; + } + return PDFColorSpace.getColorSpace(csobj, this.resources); + } + + /** + * pop a single float value off the stack. + * + * @return the float value of the top of the stack + * @throws PDFParseException + * if the value on the top of the stack + * isn't a number + */ + private float popFloat() throws PDFParseException { + if (this.stack.isEmpty() == false) { + Object obj = this.stack.pop(); + if (obj instanceof Double) { + return ((Double) obj).floatValue(); + } else { + throw new PDFParseException("Expected a number here."); + } + } + return 0; + } + + /** + * pop an array of float values off the stack. This is equivalent + * to filling an array from end to front by popping values off the + * stack. + * + * @param count + * the number of numbers to pop off the stack + * @return an array of length count + * @throws PDFParseException + * if any of the values popped off the + * stack are not numbers. + */ + private float[] popFloat(int count) throws PDFParseException { + float[] ary = new float[count]; + for (int i = count - 1; i >= 0; i--) { + ary[i] = popFloat(); + } + return ary; + } + + /** + * pop a single integer value off the stack. + * + * @return the integer value of the top of the stack + * @throws PDFParseException + * if the top of the stack isn't a number. + */ + private int popInt() throws PDFParseException { + Object obj = this.stack.pop(); + if (obj instanceof Double) { + return ((Double) obj).intValue(); + } else { + throw new PDFParseException("Expected a number here."); + } + } + + /** + * pop an array of integer values off the stack. This is equivalent + * to filling an array from end to front by popping values off the + * stack. + * + * @param count + * the number of numbers to pop off the stack + * @return an array of length count + * @throws PDFParseException + * if any of the values popped off the + * stack are not numbers. + */ + private float[] popFloatArray() throws PDFParseException { + Object obj = this.stack.pop(); + if (!(obj instanceof Object[])) { + throw new PDFParseException("Expected an [array] here."); + } + Object[] source = (Object[]) obj; + float[] ary = new float[source.length]; + for (int i = 0; i < ary.length; i++) { + if (source[i] instanceof Double) { + ary[i] = ((Double) source[i]).floatValue(); + } else { + throw new PDFParseException("This array doesn't consist only of floats."); + } + } + return ary; + } + + /** + * pop a String off the stack. + * + * @return the String from the top of the stack + * @throws PDFParseException + * if the top of the stack is not a NAME + * or STR. + */ + private String popString() throws PDFParseException { + Object obj = this.stack.pop(); + if (!(obj instanceof String)) { + throw new PDFParseException("Expected string here: " + obj.toString()); + } else { + return (String) obj; + } + } + + /** + * pop a PDFObject off the stack. + * + * @return the PDFObject from the top of the stack + * @throws PDFParseException + * if the top of the stack does not contain + * a PDFObject. + */ + @SuppressWarnings("unused") + private PDFObject popObject() throws PDFParseException { + Object obj = this.stack.pop(); + if (!(obj instanceof PDFObject)) { + throw new PDFParseException("Expected a reference here: " + obj.toString()); + } + return (PDFObject) obj; + } + + /** + * pop an array off the stack + * + * @return the array of objects that is the top element of the stack + * @throws PDFParseException + * if the top element of the stack does not + * contain an array. + */ + private Object[] popArray() throws PDFParseException { + Object obj = this.stack.pop(); + if (!(obj instanceof Object[])) { + throw new PDFParseException("Expected an [array] here: " + obj.toString()); + } + return (Object[]) obj; + } + + /** + * A class to store state needed whiel rendering. This includes the + * stroke and fill color spaces, as well as the text formatting + * parameters. + */ + class ParserState implements Cloneable { + /** the fill color space */ + PDFColorSpace fillCS; + /** the stroke color space */ + PDFColorSpace strokeCS; + /** the text paramters */ + PDFTextFormat textFormat; + + /** + * Clone the render state. + */ + @Override + public Object clone() { + ParserState newState = new ParserState(); + // no need to clone color spaces, since they are immutable + newState.fillCS = this.fillCS; + newState.strokeCS = this.strokeCS; + // we do need to clone the textFormat + newState.textFormat = (PDFTextFormat) this.textFormat.clone(); + return newState; + } + } + + @Override + protected void setStatus(int status) { + if(status == COMPLETED) { + if(!addAnnotation){ + // corresponding push in constructor PDFPage + this.cmds.addPop(); + this.cmds.addAnnotations(); + addAnnotation = true; + } + } + super.setStatus(status); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFRenderer.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFRenderer.java new file mode 100644 index 0000000000..e74d8ae7b3 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFRenderer.java @@ -0,0 +1,1038 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer; + +import java.awt.AlphaComposite; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Paint; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.GeneralPath; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.awt.image.BufferedImageOp; +import java.awt.image.ColorModel; +import java.awt.image.ConvolveOp; +import java.awt.image.ImageObserver; +import java.awt.image.IndexColorModel; +import java.awt.image.Kernel; +import java.awt.image.WritableRaster; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Stack; + +/** + * This class turns a set of PDF Commands from a PDF page into an image. It + * encapsulates the state of drawing in terms of stroke, fill, transform, + * etc., as well as pushing and popping these states. + * + * When the run method is called, this class goes through all remaining commands + * in the PDF Page and draws them to its buffered image. It then updates any + * ImageConsumers with the drawn data. + */ +public class PDFRenderer extends BaseWatchable implements Runnable { + + /** the page we were generate from */ + private PDFPage page; + /** where we are in the page's command list */ + private int currentCommand; + /** a weak reference to the image we render into. For the image + * to remain available, some other code must retain a strong reference to it. + */ + private WeakReference imageRef; + /** the graphics object for use within an iteration. Note this must be + * set to null at the end of each iteration, or the image will not be + * collected + */ + private Graphics2D g; + /** the current graphics state */ + private GraphicsState state; + /** the stack of push()ed graphics states */ + private Stack stack; + /** the total region of this image that has been written to */ + private Rectangle2D globalDirtyRegion; + /** the image observers that will be updated when this image changes */ + private final List observers; + /** the last shape we drew (to check for overlaps) */ + private GeneralPath lastShape; + private AffineTransform lastTransform; + /** the info about the image, if we need to recreate it */ + private final ImageInfo imageinfo; + /** the next time the image should be notified about updates */ + private long then = 0; + /** the sum of all the individual dirty regions since the last update */ + private Rectangle2D unupdatedRegion; + + /** how long (in milliseconds) to wait between image updates */ + public static final long UPDATE_DURATION = 200; + public static final float NOPHASE = -1000; + public static final float NOWIDTH = -1000; + public static final float NOLIMIT = -1000; + public static final int NOCAP = -1000; + public static final float[] NODASH = null; + public static final int NOJOIN = -1000; + + + /** + * create a new PDFGraphics state + * @param page the current page + * @param imageinfo the paramters of the image to render + */ + public PDFRenderer(PDFPage page, ImageInfo imageinfo, BufferedImage bi) { + super(); + + this.page = page; + this.imageinfo = imageinfo; + this.imageRef = new WeakReference(bi); + + // initialize the list of observers + this.observers = new ArrayList(); + } + + /** + * create a new PDFGraphics state, given a Graphics2D. This version + * will not create an image, and you will get a NullPointerException + * if you attempt to call getImage(). + * @param page the current page + * @param g the Graphics2D object to use for drawing + * @param imgbounds the bounds of the image into which to fit the page + * @param clip the portion of the page to draw, in page space, or null + * if the whole page should be drawn + * @param bgColor the color to draw the background of the image, or + * null for no color (0 alpha value) + */ + public PDFRenderer(PDFPage page, Graphics2D g, Rectangle imgbounds, + Rectangle2D clip, Color bgColor) { + super(); + + this.page = page; + this.g = g; + this.imageinfo = new ImageInfo(imgbounds.width, imgbounds.height, + clip, bgColor); + g.translate(imgbounds.x, imgbounds.y); + + // initialize the list of observers + this.observers = new ArrayList(); + } + + /** + * Set up the graphics transform to match the clip region + * to the image size. + */ + private void setupRendering(Graphics2D g) { + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, + RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); + + if (this.imageinfo.bgColor != null) { + g.setColor(this.imageinfo.bgColor); + g.fillRect(0, 0, this.imageinfo.width, this.imageinfo.height); + } + + g.setColor(Color.BLACK); + + // set the initial clip and transform on the graphics + AffineTransform at = getInitialTransform(); + g.transform(at); + + // set up the initial graphics state + this.state = new GraphicsState(); + this.state.cliprgn = null; + this.state.stroke = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER); + this.state.strokePaint = PDFPaint.getColorPaint(Color.black); + this.state.fillPaint = this.state.strokePaint; + this.state.fillAlpha = AlphaComposite.getInstance(AlphaComposite.SRC); + this.state.strokeAlpha = AlphaComposite.getInstance(AlphaComposite.SRC); + this.state.xform = g.getTransform(); + + // initialize the stack + this.stack = new Stack(); + + // initialize the current command + this.currentCommand = 0; + } + + /** + * push the current graphics state onto the stack. Continue working + * with the current object; calling pop() restores the state of this + * object to its state when push() was called. + */ + public void push() { + this.state.cliprgn = this.g.getClip(); + this.stack.push(this.state); + + this.state = (GraphicsState) this.state.clone(); + } + + /** + * restore the state of this object to what it was when the previous + * push() was called. + */ + public void pop() { + if(this.stack.isEmpty() == false) { + this.state = this.stack.pop(); + } + + setTransform(this.state.xform); + setClip(this.state.cliprgn); + } + + /** + * draw an outline using the current stroke and draw paint + * @param s the path to stroke + * @return a Rectangle2D to which the current region being + * drawn will be added. May also be null, in which case no dirty + * region will be recorded. + */ + public Rectangle2D stroke(GeneralPath s, boolean autoAdjustStroke) { + // TODO: consider autoAdjustStroke here instead of during parsing + // PDF specification p. 130 / > 10.6.5 + this.g.setComposite(this.state.strokeAlpha); + s = new GeneralPath(autoAdjustStrokeWidth(this.g, this.state.stroke).createStrokedShape(s)); + return this.state.strokePaint.fill(this, this.g, s); + } + + /** + * auto adjust the stroke width, according to 6.5.4, which presumes that + * the device characteristics (an image) require a single pixel wide + * line, even if the width is set to less. We determine the scaling to + * see if we would produce a line that was too small, and if so, scale + * it up to produce a graphics line of 1 pixel, or so. This matches our + * output with Adobe Reader. + * + * @param g + * @param bs + * @return + */ + private BasicStroke autoAdjustStrokeWidth(Graphics2D g, BasicStroke bs) { + AffineTransform bt = new AffineTransform(g.getTransform()); + float width = bs.getLineWidth() * (float) bt.getScaleX(); + BasicStroke stroke = bs; + if (width < 1f) { + if (bt.getScaleX() > 0.01) { + width = 1.0f / (float) bt.getScaleX(); + } else { + // prevent division by a really small number + width = stroke.getLineWidth()<1f?1.0f:stroke.getLineWidth(); + } + stroke = new BasicStroke(width, bs.getEndCap(), bs.getLineJoin(), bs.getMiterLimit(), bs.getDashArray(), bs.getDashPhase()); + } + return stroke; + } + + /** + * draw an outline. + * @param p the path to draw + * @param bs the stroke with which to draw the path + */ + public void draw(GeneralPath p, BasicStroke bs) { + this.g.setComposite(this.state.fillAlpha); + this.g.setPaint(this.state.fillPaint.getPaint()); + this.g.setStroke(autoAdjustStrokeWidth(this.g, bs)); + this.g.draw(p); + } + + /** + * fill an outline using the current fill paint + * @param s the path to fill + */ + public Rectangle2D fill(GeneralPath s) { + this.g.setComposite(this.state.fillAlpha); + if (s == null) { + GraphicsState gs = stack.peek(); + if (gs.cliprgn != null) { + s = new GeneralPath(gs.cliprgn); + } + } + return this.state.fillPaint.fill(this, this.g, s); + } + + /** + * draw an image. + * @param image the image to draw + */ + public Rectangle2D drawImage(PDFImage image) { + + BufferedImage bi; + try { + bi = image.getImage(); + }catch (PDFImageParseException e) { + // maybe it was an unsupported format, or something. + // Nothing to draw, anyway! + return new Rectangle2D.Double(); + } + + // transform must use bitmap size + AffineTransform at = new AffineTransform(1f / bi.getWidth(), 0, + 0, -1f / bi.getHeight(), + 0, 1); + + if (image.isImageMask()) { + bi = getMaskedImage(bi); + } + + Rectangle r = g.getTransform().createTransformedShape(new Rectangle(0,0,1,1)).getBounds(); + boolean isBlured = false; + + if (Configuration.getInstance().isUseBlurResizingForImages() && + bi.getType() != BufferedImage.TYPE_CUSTOM && + bi.getWidth() >= 1.75*r.getWidth() && bi.getHeight() >= 1.75*r.getHeight()){ + try { + return smartDrawImage(image, bi, r, at); + }catch (Exception e) { + // do nothing, just go on with the "default" processing + } + } + + this.g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER)); + + //Image quality is better when using texturepaint instead of drawimage + //but it is also slower :( + this.g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + // banded rendering may lead to lower memory consumption for e.g. scanned PDFs with large images + int bandSize = Configuration.getInstance().getThresholdForBandedImageRendering(); + if (bandSize > 0 && bi.getHeight() > bandSize) { + // draw in bands + int tempMax = bi.getHeight(); + for (int offset=0; offset= 1.75*r.getWidth() && bi.getHeight() >= 1.75*r.getHeight()){ + + BufferedImageOp op; + // indexed colored images need to be converted for the convolveOp + boolean colorConversion = (bi.getColorModel() instanceof IndexColorModel); + final float maxFactor = 3.5f; + final boolean RESIZE = true; + if (bi.getWidth() > maxFactor*r.getWidth() && bi.getHeight() > maxFactor*r.getHeight()){ + //First resize, otherwise we risk that we get out of heapspace + int newHeight = (int)Math.round(maxFactor*r.getHeight()); + int newWidth = (int)Math.round(maxFactor*r.getWidth()); + if (!RESIZE) { + newHeight = bi.getHeight(); + newWidth = bi.getWidth(); + } + BufferedImage resized = new BufferedImage(newWidth, + newHeight, colorConversion?BufferedImage.TYPE_INT_ARGB:bi.getType()); + Graphics2D bg = (Graphics2D) resized.getGraphics(); + bg.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + bg.drawImage(bi, 0, 0, newWidth, newHeight, null); + bi = resized; + at = new AffineTransform(1f / bi.getWidth(), 0, + 0, -1f / bi.getHeight(), + 0, 1); + + final float weight = 1.0f/16.0f; + final float[] blurKernel = { + weight, weight, weight, weight, + weight, weight, weight, weight, + weight, weight, weight, weight, + weight, weight, weight, weight, + }; + op = new ConvolveOp(new Kernel(4, 4, blurKernel), ConvolveOp.EDGE_NO_OP, null); + } + else { + final float weight = 1.0f/18.0f; + final float[] blurKernel = { + 1*weight, 2*weight, 1*weight, + 2*weight, 6*weight, 2*weight, + 1*weight, 2*weight, 1*weight + }; + if (colorConversion) { + BufferedImage colored = new BufferedImage(bi.getWidth(), + bi.getHeight(), colorConversion?BufferedImage.TYPE_INT_ARGB:bi.getType()); + Graphics2D bg = (Graphics2D) colored.getGraphics(); + bg.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + bg.drawImage(bi, 0, 0, bi.getWidth(), bi.getHeight(), null); + bi = colored; + } + op = new ConvolveOp(new Kernel(3, 3, blurKernel), ConvolveOp.EDGE_NO_OP, null); + } + + BufferedImage blured = op.createCompatibleDestImage(bi, + colorConversion?ColorModel.getRGBdefault():bi.getColorModel()); + + op.filter(bi, blured); + bi = blured; + isBlured = true; + } + + this.g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER)); + + //Image quality is better when using texturepaint instead of drawimage + //but it is also slower :( + this.g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + // banded rendering may lead to lower memory consumption for e.g. scanned PDFs with large images + int bandSize = Configuration.getInstance().getThresholdForBandedImageRendering(); + if (bandSize > 0 && bi.getHeight() > bandSize) { + // draw in bands + int tempMax = bi.getHeight(); + for (int offset=0; offset
  • Watchable.RUNNING when there are commands to be processed + *
  • Watchable.NEEDS_DATA when there are no commands to be + * processed, but the page is not yet complete + *
  • Watchable.COMPLETED when the page is done and all + * the commands have been processed + *
  • Watchable.STOPPED if the image we are rendering into + * has gone away + * + */ + @Override + public int iterate() throws Exception { + // make sure we have a page to render + if (this.page == null) { + return COMPLETED; + } + + // check if this renderer is based on a weak reference to a graphics + // object. If it is, and the graphics is no longer valid, then just quit + BufferedImage bi = null; + if (this.imageRef != null) { + bi = this.imageRef.get(); + if (bi == null) { + PDFDebugger.debug("Image went away. Stopping"); + return STOPPED; + } + + this.g = bi.createGraphics(); + } + + // check if there are any commands to parse. If there aren't, + // just return, but check if we'return really finished or not + if (this.currentCommand >= this.page.getCommandCount()) { + if (this.page.isFinished()) { + return COMPLETED; + } else { + return NEEDS_DATA; + } + } + + // find the current command + PDFCmd cmd = this.page.getCommand(this.currentCommand++); + if (cmd == null) { + // uh oh. Synchronization problem! + throw new PDFParseException("Command not found!"); + } + + // execute the command + Rectangle2D dirtyRegion = cmd.execute(this); + + // append to the global dirty region + this.globalDirtyRegion = addDirtyRegion(dirtyRegion, this.globalDirtyRegion); + this.unupdatedRegion = addDirtyRegion(dirtyRegion, this.unupdatedRegion); + + long now = System.currentTimeMillis(); + if (now > this.then || rendererFinished()) { + // now tell any observers, so they can repaint + notifyObservers(bi, this.unupdatedRegion); + this.unupdatedRegion = null; + this.then = now + UPDATE_DURATION; + } + + // if we are based on a reference to a graphics, don't hold on to it + // since that will prevent the image from being collected. + if (this.imageRef != null) { + this.g = null; + } + + // if we need to stop, it will be caught at the start of the next + // iteration. + return RUNNING; + } + + /** + * Called when iteration has stopped + */ + @Override + public void cleanup() { + this.page = null; + this.state = null; + this.stack = null; + this.globalDirtyRegion = null; + this.lastShape = null; + + this.observers.clear(); + + // keep around the image ref and image info for use in + // late addObserver() call + } + + /** + * Append a rectangle to the total dirty region of this shape + */ + private Rectangle2D addDirtyRegion(Rectangle2D region, Rectangle2D glob) { + if (region == null) { + return glob; + } else if (glob == null) { + return region; + } else { + Rectangle2D.union(glob, region, glob); + return glob; + } + } + + /** + * Determine if we are finished + */ + private boolean rendererFinished() { + if (this.page == null) { + return true; + } + + return (this.page.isFinished() && this.currentCommand == this.page.getCommandCount()); + } + + /** + * Notify the observer that a region of the image has changed + */ + private void notifyObservers(BufferedImage bi, Rectangle2D region) { + if (bi == null) { + return; + } + + int startx, starty, width, height; + int flags = 0; + + // don't do anything if nothing is there or no one is listening + if ((region == null && !rendererFinished()) || this.observers == null || + this.observers.size() == 0) { + return; + } + + if (region != null) { + // get the image data for the total dirty region + startx = (int) Math.floor(region.getMinX()); + starty = (int) Math.floor(region.getMinY()); + width = (int) Math.ceil(region.getWidth()); + height = (int) Math.ceil(region.getHeight()); + + // sometimes width or height is negative. Grrr... + if (width < 0) { + startx += width; + width = -width; + } + if (height < 0) { + starty += height; + height = -height; + } + + flags = 0; + } else { + startx = 0; + starty = 0; + width = this.imageinfo.width; + height = this.imageinfo.height; + } + if (rendererFinished()) { + flags |= ImageObserver.ALLBITS; + // forget about the Graphics -- allows the image to be + // garbage collected. + this.g = null; + } else { + flags |= ImageObserver.SOMEBITS; + } + + synchronized (this.observers) { + for (Iterator i = this.observers.iterator(); i.hasNext();) { + ImageObserver observer = i.next(); + + boolean result = observer.imageUpdate(bi, flags, + startx, starty, + width, height); + + // if result is false, the observer no longer wants to + // be notified of changes + if (!result) { + i.remove(); + } + } + } + } + + /** + * Convert an image mask into an image by painting over any pixels + * that have a value in the image with the current paint + */ + private BufferedImage getMaskedImage(BufferedImage bi) { + + // get the color of the current paint + final Paint paint = state.fillPaint.getPaint(); + if (!(paint instanceof Color)) { + // TODO - support other types of Paint + return bi; + } + + Color col = (Color) paint; + ColorModel colorModel = bi.getColorModel(); + if (colorModel instanceof IndexColorModel) { + int mapSize = ((IndexColorModel) colorModel).getMapSize(); + int pixelSize = colorModel.getPixelSize(); + if (mapSize == 2 && pixelSize == 1) { + // we have a monochrome image mask with 1 bit per pixel + // swap out the standard color with the current paint color + int[] rgbValues = new int[2]; + ((IndexColorModel) colorModel).getRGBs(rgbValues); + byte[] colorComponents = null; + if (rgbValues[0] == 0xff000000) { + // normal case color at 0 + colorComponents = new byte[]{ + (byte) col.getRed(), + (byte) col.getGreen(), + (byte) col.getBlue(), + (byte) col.getAlpha(), + 0, 0, 0, 0 // the background is transparent + }; + } + else if (rgbValues[1] == 0xff000000){ + // alternate case color at 1 + colorComponents = new byte[]{ + 0, 0, 0, 0, // the background is transparent + (byte) col.getRed(), + (byte) col.getGreen(), + (byte) col.getBlue(), + (byte) col.getAlpha() + }; + } + + if (colorComponents != null) { + // replace mapped colors + int startIndex = 0; + boolean hasAlpha = true; + ColorModel replacementColorModel = new IndexColorModel(pixelSize, mapSize, colorComponents, startIndex, hasAlpha); + WritableRaster raster = bi.getRaster(); + BufferedImage adaptedImage = new BufferedImage(replacementColorModel, raster, false, null); + return adaptedImage; + } + else { + return bi; // no color replacement + } + } + } + + // format as 8 bits each of ARGB + int paintColor = col.getAlpha() << 24; + paintColor |= col.getRed() << 16; + paintColor |= col.getGreen() << 8; + paintColor |= col.getBlue(); + + // transparent (alpha = 1) + int noColor = 0; + + // get the coordinates of the source image + int startX = bi.getMinX(); + int startY = bi.getMinY(); + int width = bi.getWidth(); + int height = bi.getHeight(); + + // create a destion image of the same size + BufferedImage dstImage = + new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + + // copy the pixels row by row + for (int i = 0; i < height; i++) { + int[] srcPixels = new int[width]; + int[] dstPixels = new int[srcPixels.length]; + + // read a row of pixels from the source + bi.getRGB(startX, startY + i, width, 1, srcPixels, 0, height); + + // figure out which ones should get painted + for (int j = 0; j < srcPixels.length; j++) { + if (srcPixels[j] == 0xff000000) { + dstPixels[j] = paintColor; + } else { + dstPixels[j] = noColor; + } + } + + // write the destination image + dstImage.setRGB(startX, startY + i, width, 1, dstPixels, 0, height); + } + + return dstImage; + } + + class GraphicsState implements Cloneable { + + /** the clip region */ + Shape cliprgn; + /** the current stroke */ + BasicStroke stroke; + /** the current paint for drawing strokes */ + PDFPaint strokePaint; + /** the current paint for filling shapes */ + PDFPaint fillPaint; + /** the current compositing alpha for stroking */ + AlphaComposite strokeAlpha; + /** the current compositing alpha for filling */ + AlphaComposite fillAlpha; + /** the current transform */ + AffineTransform xform; + + /** Clone this Graphics state. + * + * Note that cliprgn is not cloned. It must be set manually from + * the current graphics object's clip + */ + @Override + public Object clone() { + GraphicsState cState = new GraphicsState(); + cState.cliprgn = null; + + // copy immutable fields + cState.strokePaint = this.strokePaint; + cState.fillPaint = this.fillPaint; + cState.strokeAlpha = this.strokeAlpha; + cState.fillAlpha = this.fillAlpha; + + // clone mutable fields + cState.stroke = new BasicStroke(this.stroke.getLineWidth(), + this.stroke.getEndCap(), + this.stroke.getLineJoin(), + this.stroke.getMiterLimit(), + this.stroke.getDashArray(), + this.stroke.getDashPhase()); + cState.xform = (AffineTransform) this.xform.clone(); + + return cState; + } + } + + /************************************************************************* + * @return Returns the lastTransform. + ************************************************************************/ + public AffineTransform getLastTransform() { + return this.lastTransform; + } + + /************************************************************************* + * Remember the current transformation + ************************************************************************/ + public void rememberTransformation() { + this.lastTransform = this.state.xform; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFShapeCmd.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFShapeCmd.java new file mode 100644 index 0000000000..64d22ebe66 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFShapeCmd.java @@ -0,0 +1,204 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer; +import java.awt.BasicStroke; +import java.awt.geom.AffineTransform; +import java.awt.geom.GeneralPath; +import java.awt.geom.PathIterator; +import java.awt.geom.Rectangle2D; + +/** +* Encapsulates a path. Also contains extra fields and logic to check +* for consecutive abutting anti-aliased regions. We stroke the shared +* line between these regions again with a 1-pixel wide line so that +* the background doesn't show through between them. +* +* @author Mike Wessler +*/ +public class PDFShapeCmd extends PDFCmd { + /** stroke the outline of the path with the stroke paint */ + public static final int STROKE = 1; + /** fill the path with the fill paint */ + public static final int FILL = 2; + /** perform both stroke and fill */ + public static final int BOTH = 3; + /** set the clip region to the path */ + public static final int CLIP = 4; + /** base path */ + private final GeneralPath gp; + /** the style */ + private final int style; + /** the bounding box of the path */ + // private Rectangle2D bounds; + /** the stroke style for the anti-antialias stroke */ + BasicStroke againstroke = new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL); + private boolean autoAdjustStroke = false; + + /** + * create a new PDFShapeCmd and check it against the previous one + * to find any shared edges. + * + * @param gp + * the path + * @param style + * the style: an OR of STROKE, FILL, or CLIP. As a + * convenience, BOTH = STROKE | FILL. + */ + public PDFShapeCmd(GeneralPath gp, int style, boolean autoAdjustStroke) { + this.gp = gp; + this.style = style; + this.autoAdjustStroke = autoAdjustStroke; + } + + /** + * perform the stroke and record the dirty region + */ + @Override + public Rectangle2D execute(PDFRenderer state) { + Rectangle2D rect = null; + if ((this.style & FILL) != 0) { + rect = state.fill(this.gp); + GeneralPath strokeagain = checkOverlap(state); + if (strokeagain != null) { + state.draw(strokeagain, this.againstroke); + } + if (this.gp != null) { + state.setLastShape(this.gp); + state.rememberTransformation(); + } + } + if ((this.style & STROKE) != 0) { + Rectangle2D strokeRect = state.stroke(this.gp, autoAdjustStroke); + if (rect == null) { + rect = strokeRect; + } else { + rect = rect.createUnion(strokeRect); + } + } + if ((this.style & CLIP) != 0) { + state.clip(this.gp); + } + return rect; + } + + /** + * Check for overlap with the previous shape to make anti-aliased shapes + * that are near each other look good + */ + private GeneralPath checkOverlap(PDFRenderer state) { + if (this.style == FILL && this.gp != null && state.getLastShape() != null) { + float mypoints[] = new float[16]; + float prevpoints[] = new float[16]; + int mycount = getPoints(this.gp, mypoints, state.getTransform()); + int prevcount = getPoints(state.getLastShape(), prevpoints, state.getLastTransform()); + // now check mypoints against prevpoints for opposite pairs: + if (mypoints != null && prevpoints != null) { + for (int i = 0; i < prevcount; i += 4) { + for (int j = 0; j < mycount; j += 4) { + if ((Math.abs(mypoints[j + 2] - prevpoints[i]) < 0.01 && Math.abs(mypoints[j + 3] - prevpoints[i + 1]) < 0.01 && Math.abs(mypoints[j] - prevpoints[i + 2]) < 0.01 && Math + .abs(mypoints[j + 1] - prevpoints[i + 3]) < 0.01)) { + // it seems that need to use the original points location (without the Affine Transform) + mypoints = new float[16]; + getPoints(this.gp, mypoints, null);// without AffineTransform + + GeneralPath strokeagain = new GeneralPath(); + strokeagain.moveTo(mypoints[j], mypoints[j + 1]); + strokeagain.lineTo(mypoints[j + 2], mypoints[j + 3]); + return strokeagain; + } + } + } + } + } + // no issues + return null; + } + + /** + * Get an array of 16 points from a path + * + * @return the number of points we actually got + */ + private int getPoints(GeneralPath path, float[] mypoints, AffineTransform at) { + int count = 0; + float x = 0; + float y = 0; + float startx = 0; + float starty = 0; + float[] coords = new float[6]; + PathIterator pi = path.getPathIterator(at); + while (!pi.isDone()) { + if (count >= mypoints.length) { + mypoints = null; + break; + } + int pathtype = pi.currentSegment(coords); + switch (pathtype) { + case PathIterator.SEG_MOVETO: + startx = x = coords[0]; + starty = y = coords[1]; + break; + case PathIterator.SEG_LINETO: + mypoints[count++] = x; + mypoints[count++] = y; + x = mypoints[count++] = coords[0]; + y = mypoints[count++] = coords[1]; + break; + case PathIterator.SEG_QUADTO: + x = coords[2]; + y = coords[3]; + break; + case PathIterator.SEG_CUBICTO: + x = mypoints[4]; + y = mypoints[5]; + break; + case PathIterator.SEG_CLOSE: + mypoints[count++] = x; + mypoints[count++] = y; + x = mypoints[count++] = startx; + y = mypoints[count++] = starty; + break; + } + pi.next(); + } + return count; + } + + /** + * Get detailed information about this shape + */ + @Override + public String getDetails() { + StringBuffer sb = new StringBuffer(); + Rectangle2D b = this.gp.getBounds2D(); + sb.append("ShapeCommand at: " + b.getX() + ", " + b.getY() + "\n"); + sb.append("Size: " + b.getWidth() + " x " + b.getHeight() + "\n"); + sb.append("Mode: "); + if ((this.style & FILL) != 0) { + sb.append("FILL "); + } + if ((this.style & STROKE) != 0) { + sb.append("STROKE "); + } + if ((this.style & CLIP) != 0) { + sb.append("CLIP"); + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFStringUtil.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFStringUtil.java new file mode 100644 index 0000000000..76961d7904 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFStringUtil.java @@ -0,0 +1,230 @@ +/* + * Copyright 2008 Pirion Systems Pty Ltd, 139 Warry St, + * Fortitude Valley, Queensland, Australia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer; + +import java.io.*; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; + +/** + *

    Utility methods for dealing with PDF Strings, such as: + *

      + *
    • {@link #asTextString(String) converting to text strings} + *
    • {@link #asPDFDocEncoded(String) converting to PDFDocEncoded strings} + *
    • {@link #asUTF16BEEncoded converting to UTF-16BE strings} + *
    • converting basic strings between {@link #asBytes(String) byte} and + * {@link #asBasicString(byte[], int, int) string} representations + *

    + * + *

    We refer to basic strings as those corresponding to the PDF 'string' type. + * PDFRenderer represents these as {@link String}s, though this is somewhat + * deceiving, as they are, effectively, just sequences of bytes, although byte + * values <= 127 do correspond to the ASCII character set. Outside of this, + * the 'string' type, as repesented by basic strings do not possess any + * character set or encoding, and byte values >= 128 are entirely acceptable. + * For a basic string as represented by a String, each character has a value + * less than 256 and is represented in the String as if the bytes represented as + * it were in ISO-8859-1 encoding. This, however, is merely for convenience. For + * strings that are user visible, and that don't merely represent some + * identifying token, the PDF standard employs a 'text string' type that offers + * the basic string as an encoding of in either UTF-16BE (with a byte order + * marking) or a specific 8-byte encoding, PDFDocEncoding. Using a basic string + * without conversion when the actual type is a 'text string' is erroneous + * (though without consequence if the string consists only of ASCII + * alphanumeric values). Care must be taken to either convert basic strings to + * text strings (also expressed as a String) when appropriate, using either the + * methods in this class, or {@link PDFObject#getTextStringValue()}}. For + * strings that are 'byte strings', {@link #asBytes(String)} or {@link + * PDFObject#getStream()} should be used.

    . + * + * @author Luke Kirby + */ +public class PDFStringUtil { + + /** + *

    Take a basic PDF string and determine if it is in UTF-16BE encoding + * by looking at the lead characters for a byte order marking (BOM). If it + * appears to be UTF-16BE, we return the string representation of the + * UTF-16BE encoding of those bytes. If the BOM is not present, the bytes + * from the input string are decoded using the PDFDocEncoding charset.

    + * + *

    From the PDF Reference 1.7, p158: + * + *

    The text string type is used for character strings that are + * encoded in either PDFDocEncoding or the UTF-16BE Unicode character + * encoding scheme. PDFDocEncoding can encode all of the ISO Latin 1 + * character set and is documented in Appendix D. UTF-16BE can encode all + * Unicode characters. UTF-16BE and Unicode character encoding are + * described in the Unicode Standard by the Unicode Consortium (see the + * Bibliography). Note that PDFDocEncoding does not support all Unicode + * characters whereas UTF-16BE does.
    + *

    + * + * @param basicString the basic PDF string, as offered by {@link + * PDFObject#getStringValue()} + * @return either the original input, or the input decoded as UTF-16 + */ + public static String asTextString(String basicString) { + if (basicString == null) { + return null; + } + + if (basicString.length() >= 2) { + if ((basicString.charAt(0) == (char) 0xFE + && basicString.charAt(1) == (char) 0xFF)) { + // found the BOM! + return asUTF16BEEncoded(basicString); + } + } + + // it's not UTF16-BE encoded, so it must be + return asPDFDocEncoded(basicString); + } + + /** + * Take a basic PDF string and produce a string of its bytes as encoded in + * PDFDocEncoding. The PDFDocEncoding is described in the PDF Reference. + * + * @param basicString the basic PDF string, as offered by {@link + * PDFObject#getStringValue()} + * @return the decoding of the string's bytes in PDFDocEncoding + */ + public static String asPDFDocEncoded(String basicString) { + final StringBuilder buf = new StringBuilder(basicString.length()); + for (int i = 0; i < basicString.length(); ++i) { + final char c = PDF_DOC_ENCODING_MAP[basicString.charAt(i) & 0xFF]; + buf.append(c); + } + return buf.toString(); + } + + public byte[] toPDFDocEncoded(String string) + throws CharacterCodingException { + // we can just grab array since we know that if charset completes + // without error then there's the output buffer will be exactly + // correct in size, since there's only ever 1 byte for one char. + return new PDFDocCharsetEncoder().encode(CharBuffer.wrap(string)). + array(); + } + + /** + * Take a basic PDF string and produce a string from its bytes as an + * UTF16-BE encoding. The first 2 bytes are presumed to be the big-endian + * byte markers, 0xFE and 0xFF; that is not checked by this method. + * + * @param basicString the basic PDF string, as offered by {@link + * PDFObject#getStringValue()} + * @return the decoding of the string's bytes in UTF16-BE + */ + public static String asUTF16BEEncoded(String basicString) { + try { + return new String(asBytes(basicString), + 2, basicString.length() - 2, "UTF-16BE"); + } catch (UnsupportedEncodingException e) { + // UTF-16BE should always be available + throw new RuntimeException("No UTF-16BE charset!"); + } + } + + /** + * Get the corresponding byte array for a basic string. This is effectively + * the char[] array cast to bytes[], as chars in basic strings only use the + * least significant byte. + * + * @param basicString the basic PDF string, as offered by {@link + * PDFObject#getStringValue()} + * @return the bytes corresponding to its characters + */ + public static byte[] asBytes(String basicString) { + final byte[] b = new byte[basicString.length()]; + for (int i = 0; i < b.length; ++i) { + b[i] = (byte) basicString.charAt(i); + } + return b; + } + + /** + * Create a basic string from bytes. This is effectively the byte array + * cast to a char array and turned into a String. + * @param bytes the source of the bytes for the basic string + * @param offset the offset into butes where the string starts + * @param length the number of bytes to turn into a string + * @return the corresponding string + */ + public static String asBasicString( + byte[] bytes, int offset, int length) { + final char[] c = new char[length]; + for (int i = 0; i < c.length; ++i) { + c[i] = (char) bytes[i + offset]; + } + return new String(c); + } + + /** + * Create a basic string from bytes. This is effectively the byte array + * cast to a char array and turned into a String. + * @param bytes the bytes, all of which are used + * @return the corresponding string + */ + public static String asBasicString(byte[] bytes) { + return asBasicString(bytes, 0, bytes.length); + } + + /** + * Maps from PDFDocEncoding bytes to unicode characters. Table generated + * by PDFDocEncodingMapGenerator. + */ + final static char[] PDF_DOC_ENCODING_MAP = new char[] { + 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, //00-07 + 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F, //08-0F + 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017, //10-17 + 0x02D8, 0x02C7, 0x02C6, 0x02D9, 0x02DD, 0x02DB, 0x02DA, 0x02DC, //18-1F + 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027, //20-27 + 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F, //28-2F + 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, //30-37 + 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F, //38-3F + 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, //40-47 + 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, //48-4F + 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, //50-57 + 0x0058, 0x0059, 0x005A, 0x005B, 0x005C, 0x005D, 0x005E, 0x005F, //58-5F + 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, //60-67 + 0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, //68-6F + 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, //70-77 + 0x0078, 0x0079, 0x007A, 0x007B, 0x007C, 0x007D, 0x007E, 0xFFFD, //78-7F + 0x2022, 0x2020, 0x2021, 0x2026, 0x2014, 0x2013, 0x0192, 0x2044, //80-87 + 0x2039, 0x203A, 0x2212, 0x2030, 0x201E, 0x201C, 0x201D, 0x2018, //88-8F + 0x2019, 0x201A, 0x2122, 0xFB01, 0xFB02, 0x0141, 0x0152, 0x0160, //90-97 + 0x0178, 0x017D, 0x0131, 0x0142, 0x0153, 0x0161, 0x017E, 0xFFFD, //98-9F + 0x20AC, 0x00A1, 0x00A2, 0x00A3, 0x00A4, 0x00A5, 0x00A6, 0x00A7, //A0-A7 + 0x00A8, 0x00A9, 0x00AA, 0x00AB, 0x00AC, 0xFFFD, 0x00AE, 0x00AF, //A8-AF + 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5, 0x00B6, 0x00B7, //B0-B7 + 0x00B8, 0x00B9, 0x00BA, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00BF, //B8-BF + 0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x00C7, //C0-C7 + 0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF, //C8-CF + 0x00D0, 0x00D1, 0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x00D7, //D0-D7 + 0x00D8, 0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x00DD, 0x00DE, 0x00DF, //D8-DF + 0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x00E7, //E0-E7 + 0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x00EC, 0x00ED, 0x00EE, 0x00EF, //E8-EF + 0x00F0, 0x00F1, 0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x00F7, //F0-F7 + 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x00FD, 0x00FE, 0x00FF, //F8-FF + }; + + + +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFTextFormat.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFTextFormat.java new file mode 100644 index 0000000000..8777dd2522 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFTextFormat.java @@ -0,0 +1,390 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer; + +import java.awt.Color; +import java.awt.geom.AffineTransform; +import java.awt.geom.GeneralPath; +import java.awt.geom.Point2D; +import java.util.Iterator; +import java.util.List; + +import com.github.librepdf.pdfrenderer.font.PDFFont; +import com.github.librepdf.pdfrenderer.font.PDFGlyph; + +/** +* a class encapsulating the text state +* +* @author Mike Wessler +*/ +public class PDFTextFormat implements Cloneable { + /** character spacing */ + private float tc = 0; + /** word spacing */ + private float tw = 0; + /** horizontal scaling */ + private float th = 1; + /** leading */ + private float tl = 0; + /** rise amount */ + private float tr = 0; + /** text mode */ + private int tm = PDFShapeCmd.FILL; + /** text knockout */ + private float tk = 0; + /** current matrix transform */ + private final AffineTransform cur; + /** matrix transform at start of line */ + private AffineTransform line; + /** font */ + private PDFFont font; + /** font size */ + private float fsize = 1; + /** are we between BT and ET? */ + private boolean inuse = false; + // private Object array[]= new Object[1]; + /** build text rep of word */ + private final StringBuffer word = new StringBuffer(); + // this is where we build and keep the word list for this page. + /** start location of the hunk of text */ + private final Point2D.Float wordStart; + /** location of the end of the previous hunk of text */ + private final Point2D.Float prevEnd; + + /** + * create a new PDFTextFormat, with initial values + */ + public PDFTextFormat() { + this.cur = new AffineTransform(); + this.line = new AffineTransform(); + this.wordStart = new Point2D.Float(-100, -100); + this.prevEnd = new Point2D.Float(-100, -100); + this.tc = this.tw = this.tr = this.tk = 0; + this.tm = PDFShapeCmd.FILL; + this.th = 1; + } + + /** + * reset the PDFTextFormat for a new run + */ + public void reset() { + this.cur.setToIdentity(); + this.line.setToIdentity(); + this.inuse = true; + this.word.setLength(0); + } + + /** + * end a span of text + */ + public void end() { + this.inuse = false; + } + + /** get the char spacing */ + public float getCharSpacing() { + return this.tc; + } + + /** set the character spacing */ + public void setCharSpacing(float spc) { + this.tc = spc; + } + + /** get the word spacing */ + public float getWordSpacing() { + return this.tw; + } + + /** set the word spacing */ + public void setWordSpacing(float spc) { + this.tw = spc; + } + + /** + * Get the horizontal scale + * + * @return the horizontal scale, in percent + */ + public float getHorizontalScale() { + return this.th * 100; + } + + /** + * set the horizontal scale. + * + * @param scl + * the horizontal scale, in percent (100=normal) + */ + public void setHorizontalScale(float scl) { + this.th = scl / 100; + } + + /** get the leading */ + public float getLeading() { + return this.tl; + } + + /** set the leading */ + public void setLeading(float spc) { + this.tl = spc; + } + + /** get the font */ + public PDFFont getFont() { + return this.font; + } + + /** get the font size */ + public float getFontSize() { + return this.fsize; + } + + /** set the font and size */ + public void setFont(PDFFont f, float size) { + this.font = f; + this.fsize = size; + } + + /** + * Get the mode of the text + */ + public int getMode() { + return this.tm; + } + + /** + * set the mode of the text. The correspondence of m to mode is + * show in the following table. m is a value from 0-7 in binary: + * + * 000 Fill + * 001 Stroke + * 010 Fill + Stroke + * 011 Nothing + * 100 Fill + Clip + * 101 Stroke + Clip + * 110 Fill + Stroke + Clip + * 111 Clip + * + * Therefore: Fill corresponds to the low bit being 0; Clip + * corresponds to the hight bit being 1; and Stroke corresponds + * to the middle xor low bit being 1. + */ + public void setMode(int m) { + int mode = 0; + if ((m & 0x1) == 0) { + mode |= PDFShapeCmd.FILL; + } + if ((m & 0x4) != 0) { + mode |= PDFShapeCmd.CLIP; + } + if (((m & 0x1) ^ ((m & 0x2) >> 1)) != 0) { + mode |= PDFShapeCmd.STROKE; + } + this.tm = mode; + } + + /** + * Set the mode from another text format mode + * + * @param mode + * the text render mode using the + * codes from PDFShapeCmd and not the wacky PDF codes + */ + public void setTextFormatMode(int mode) { + this.tm = mode; + } + + /** + * Get the rise + */ + public float getRise() { + return this.tr; + } + + /** + * set the rise + */ + public void setRise(float spc) { + this.tr = spc; + } + + /** + * perform a carriage return + */ + public void carriageReturn() { + carriageReturn(0, -this.tl); + } + + /** + * perform a carriage return by translating by x and y. The next + * carriage return will be relative to the new location. + */ + public void carriageReturn(float x, float y) { + this.line.concatenate(AffineTransform.getTranslateInstance(x, y)); + this.cur.setTransform(this.line); + } + + /** + * Get the current transform + */ + public AffineTransform getTransform() { + return this.cur; + } + + /** + * set the transform matrix directly + */ + public void setMatrix(float[] matrix) { + this.line = new AffineTransform(matrix); + this.cur.setTransform(this.line); + } + + /** + * add some text to the page. + * + * @param cmds + * the PDFPage to add the commands to + * @param text + * the text to add + */ + public void doText(PDFPage cmds, String text, boolean autoAdjustStroke) { + Point2D.Float zero = new Point2D.Float(); + AffineTransform scale = new AffineTransform(this.fsize * this.th, 0, /* 0 */ + 0, this.fsize, /* 0 */ + 0, this.tr /* 1 */); + AffineTransform at = new AffineTransform(); + List l = this.font.getGlyphs(text); + if (PDFDebugger.SHOW_TEXT_ANCHOR) { + if (PDFDebugger.DEBUG_TEXT) { + PDFDebugger.debug("POINT count: " + l.size()); + } + } + for (Iterator i = l.iterator(); i.hasNext();) { + PDFGlyph glyph = i.next(); + at.setTransform(this.cur); + at.concatenate(scale); + if (PDFDebugger.SHOW_TEXT_REGIONS) { + GeneralPath path = new GeneralPath(); + path.moveTo(0, 0); + path.lineTo(1, 0); + path.lineTo(1, 1); + path.lineTo(0, 1); + path.lineTo(0, 0); + path.closePath(); + path = (GeneralPath) path.createTransformedShape(at); + if (PDFDebugger.DEBUG_TEXT) { + PDFDebugger.debug("BOX " + path.getBounds()); + } + PDFCmd lastColor = cmds.findLastCommand(PDFFillPaintCmd.class); + if (PDFDebugger.DEBUG_TEXT) { + PDFDebugger.debug("BOX " + lastColor); + } + cmds.addFillPaint(PDFPaint.getColorPaint(new Color(160, 160, 255))); + cmds.addPath(path, PDFShapeCmd.FILL, autoAdjustStroke); + if (lastColor != null) { + cmds.addCommand(lastColor); + } + } + Point2D advance = glyph.getAdvance(); + if (!PDFDebugger.DISABLE_TEXT) { + advance = glyph.addCommands(cmds, at, this.tm); + } + double advanceX = (advance.getX() * this.fsize) + this.tc; + double advanceY = advance.getY() + this.fsize; + if (glyph.getChar() == ' ') { + advanceX += this.tw; + } + advanceX *= this.th; + if (PDFDebugger.SHOW_TEXT_ANCHOR) { + AffineTransform at2 = new AffineTransform(); + at2.setTransform(this.cur); + GeneralPath path = new GeneralPath(); + path.moveTo(0, 0); + path.lineTo(6, 0); + path.lineTo(6, 6); + path.lineTo(0, 6); + path.lineTo(0, 0); + path.closePath(); + path = (GeneralPath) path.createTransformedShape(at2); + if (PDFDebugger.DEBUG_TEXT) { + PDFDebugger.debug("POINT " + advance); + } + PDFCmd lastColor = cmds.findLastCommand(PDFFillPaintCmd.class); + cmds.addFillPaint(PDFPaint.getColorPaint(new Color(255, 0, 0))); + cmds.addPath(path, PDFShapeCmd.FILL, autoAdjustStroke); + if (lastColor != null) { + cmds.addCommand(lastColor); + } + } + this.cur.translate(advanceX, advance.getY()); + } + this.cur.transform(zero, this.prevEnd); + } + + /** + * add some text to the page. + * + * @param cmds + * the PDFPage to add the commands to + * @param ary + * an array of Strings and Doubles, where the Strings + * represent text to be added, and the Doubles represent kerning + * amounts. + */ + public void doText(PDFPage cmds, Object ary[], boolean autoAdjustStroke) throws PDFParseException { + for (int i = 0; i < ary.length; i++) { + if (ary[i] instanceof String) { + doText(cmds, (String) ary[i], autoAdjustStroke); + } else if (ary[i] instanceof Double) { + float val = ((Double) ary[i]).floatValue() / 1000f; + this.cur.translate(-val * this.fsize * this.th, 0); + } else { + throw new PDFParseException("Bad element in TJ array"); + } + } + } + + /** + * finish any unfinished words. TODO: write this! + */ + public void flush() { + // TODO: finish any unfinished words + } + + /** + * Clone the text format + */ + @Override + public Object clone() { + PDFTextFormat newFormat = new PDFTextFormat(); + // copy values + newFormat.setCharSpacing(getCharSpacing()); + newFormat.setWordSpacing(getWordSpacing()); + newFormat.setHorizontalScale(getHorizontalScale()); + newFormat.setLeading(getLeading()); + newFormat.setTextFormatMode(getMode()); + newFormat.setRise(getRise()); + // copy immutable fields + newFormat.setFont(getFont(), getFontSize()); + // clone transform (mutable) + // newFormat.getTransform().setTransform(getTransform()); + return newFormat; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFXref.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFXref.java new file mode 100644 index 0000000000..6f587e0704 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PDFXref.java @@ -0,0 +1,153 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer; + +import java.lang.ref.SoftReference; + +/** + * a cross reference representing a line in the PDF cross referencing + * table. + *

    + * There are two forms of the PDFXref, destinguished by absolutely nothing. + * The first type of PDFXref is used as indirect references in a PDFObject. + * In this type, the id is an index number into the object cross reference + * table. The id will range from 0 to the size of the cross reference + * table. + *

    + * The second form is used in the Java representation of the cross reference + * table. In this form, the id is the file position of the start of the + * object in the PDF file. See the use of both of these in the + * PDFFile.dereference() method, which takes a PDFXref of the first form, + * and uses (internally) a PDFXref of the second form. + *

    + * This is an unhappy state of affairs, and should be fixed. Fortunatly, + * the two uses have already been factored out as two different methods. + * + * @author Mike Wessler + */ +public class PDFXref { + + private int id; + private int generation; + private final boolean compressed; + + // this field is only used in PDFFile.objIdx + private SoftReference reference = null; + + /** + * create a new PDFXref, given a parsed id and generation. + */ + public PDFXref(int id, int gen) { + this.id = id; + this.generation = gen; + this.compressed = false; + } + + /** + * create a new PDFXref, given a parsed id, compressedObjId and index + */ + public PDFXref(int id, int gen, boolean compressed) { + this.id = id; + this.generation = gen; + this.compressed = compressed; + } + + /** + * create a new PDFXref, given a sequence of bytes representing the + * fixed-width cross reference table line + */ + public PDFXref(byte[] line) { + if (line == null) { + this.id = -1; + this.generation = -1; + } else { + this.id = Integer.parseInt(new String(line, 0, 10).trim()); + this.generation = Integer.parseInt(new String(line, 11, 5).trim()); + } + this.compressed = false; + } + + /** + * get the character index into the file of the start of this object + */ + public int getFilePos() { + return this.id; + } + + /** + * get the generation of this object + */ + public int getGeneration() { + return this.generation; + } + + /** + * get the generation of this object + */ + public int getIndex() { + return this.generation; + } + + /** + * get the object number of this object + */ + public int getID() { + return this.id; + } + + /** + * get compressed flag of this object + */ + public boolean getCompressed() { + return this.compressed; + } + + + /** + * Get the object this reference refers to, or null if it hasn't been + * set. + * @return the object if it exists, or null if not + */ + public PDFObject getObject() { + if (this.reference != null) { + return this.reference.get(); + } + + return null; + } + + /** + * Set the object this reference refers to. + */ + public void setObject(PDFObject obj) { + this.reference = new SoftReference(obj); + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof PDFXref) && + ((PDFXref)obj).id == id && + ((PDFXref)obj).generation == generation; + } + + @Override + public int hashCode() { + return id ^ (generation << 8); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PdfSubByteSampleModel.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PdfSubByteSampleModel.java new file mode 100644 index 0000000000..7e50ff6bbd --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/PdfSubByteSampleModel.java @@ -0,0 +1,143 @@ +/* + * $Id: PdfSubByteSampleModel.java,v 1.1 2010-05-23 22:07:05 lujke Exp $ + * + * Copyright 2010 Pirion Systems Pty Ltd, 139 Warry St, + * Fortitude Valley, Queensland, Australia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer; + +import java.awt.image.DataBuffer; +import java.awt.image.SampleModel; + +/** + * Provides a read-only sample-model where components are less than a byte in + * width while allowing for pixels to cross byte-boundaries. For example, this + * allows 2 pixels made of 3 4-bit-bands (p[pixel,band]) + * to be stored in 3 bytes as p[0,1] p[0,2] | p[0,3] p[1,0] | p[1,1] p[1,2]. + * {@link java.awt.image.MultiPixelPackedSampleModel}, which allows for sub-byte + * components, does not allow for such byte spanning, while the PDF + * specification does permit it -- hence the existence of this class. + * + * @author Luke Kirby, Pirion Systems + */ +public class PdfSubByteSampleModel extends SampleModel +{ + private final int transferBytesPerPixel; + private final int storageBitsPerPixel; + private final int bitsPerLine; + private final int bitsPerBand; + private final int componentMask; + private final int[] sampleSize; + private final int ignoredBitsPerComponentPerByte; + + public PdfSubByteSampleModel(int w, int h, int numComponents, int bitsPerComponent) + { + super(DataBuffer.TYPE_BYTE, w, h, numComponents); + assert bitsPerComponent < 8 : "This is designed just for use with per-component sizes of less than 8 bits; " + + "you should probably use PixelInterleavedSampleModel"; + assert bitsPerComponent == 1 || bitsPerComponent == 2 || bitsPerComponent == 4 : + "we don't want to grab components across byte boundaries"; + transferBytesPerPixel = (numComponents * bitsPerComponent + 7) / 8; + storageBitsPerPixel = numComponents * bitsPerComponent; + // account for possible bits of padding on the end + bitsPerLine = 8 * ((storageBitsPerPixel * w + 7) / 8); + this.bitsPerBand = bitsPerComponent; + componentMask = (1 << this.bitsPerBand) - 1; + + sampleSize = new int[numComponents]; + for (int i = 0; i < sampleSize.length; ++i) { + sampleSize[i] = bitsPerComponent; + } + ignoredBitsPerComponentPerByte = 8 - bitsPerBand; + } + + @Override + public int getNumDataElements() + { + return transferBytesPerPixel; + } + + @Override + public Object getDataElements(int x, int y, Object obj, DataBuffer data) + { + byte[] elements = obj != null ? (byte[])obj : new byte[numBands]; + int bitIndex = y * bitsPerLine + storageBitsPerPixel * x; + for (int i = 0; i < elements.length; ++i) { + elements[i] = (byte) getComponent(data, bitIndex); + bitIndex += bitsPerBand; + } + return elements; + } + + private int getComponent(DataBuffer data, int aBitIndex) + { + final int boffset = aBitIndex >> 3; // == aBitIndex / 8 + final int b = data.getElem(boffset); + final int bitIndexInB = aBitIndex & 7; + final int shift = ignoredBitsPerComponentPerByte - bitIndexInB; + return (b >>> shift) & componentMask; + } + + @Override + public void setDataElements(int x, int y, Object obj, DataBuffer data) + { + throw new UnsupportedOperationException("read only"); + } + + @Override + public int getSample(int x, int y, int b, DataBuffer data) + { + return getComponent(data, y * bitsPerLine + storageBitsPerPixel * x + bitsPerBand * b); + } + + @Override + public void setSample(int x, int y, int b, int s, DataBuffer data) + { + throw new UnsupportedOperationException("read only"); + + } + + @Override + public SampleModel createCompatibleSampleModel(int w, int h) + { + throw new UnsupportedOperationException("Not required"); + } + + @Override + public SampleModel createSubsetSampleModel(int[] bands) + { + throw new UnsupportedOperationException("Not required"); + } + + @Override + public DataBuffer createDataBuffer() + { + throw new UnsupportedOperationException("Not required"); + } + + @Override + public int[] getSampleSize() + { + return sampleSize; + } + + @Override + public int getSampleSize(int band) + { + return bitsPerBand; + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/RefImage.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/RefImage.java new file mode 100644 index 0000000000..93cd087a76 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/RefImage.java @@ -0,0 +1,53 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer; + +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; + +/** + * A BufferedImage subclass that holds a strong reference to its graphics + * object. This means that the graphics will never go away as long as + * someone holds a reference to this image, and createGraphics() and + * getGraphics() can be called multiple times safely, and will always return + * the same graphics object. + */ +public class RefImage extends BufferedImage { + + /** a strong reference to the graphics object */ + private Graphics2D g; + + /** Creates a new instance of RefImage */ + public RefImage(int width, int height, int type) { + super(width, height, type); + } + + /** + * Create a graphics object only if it is currently null, otherwise + * return the existing graphics object. + */ + @Override + public Graphics2D createGraphics() { + if (this.g == null) { + this.g = super.createGraphics(); + } + + return this.g; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/Watchable.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/Watchable.java new file mode 100644 index 0000000000..1c2186e6fe --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/Watchable.java @@ -0,0 +1,71 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer; + +/** + * An interface for rendering or parsing, which can be stopped and started. + */ +public interface Watchable { + + /** the possible statuses */ + public static final int UNKNOWN = 0; + public static final int NOT_STARTED = 1; + public static final int PAUSED = 2; + public static final int NEEDS_DATA = 3; + public static final int RUNNING = 4; + public static final int STOPPED = 5; + public static final int COMPLETED = 6; + public static final int ERROR = 7; + + /** + * Get the status of this watchable + * + * @return one of the well-known statuses + */ + public int getStatus(); + + /** + * Stop this watchable. Stop will cause all processing to cease, + * and the watchable to be destroyed. + */ + public void stop(); + + /** + * Start this watchable and run until it is finished or stopped. + * Note the watchable may be stopped if go() with a + * different time is called during execution. + */ + public void go(); + + /** + * Start this watchable and run for the given number of steps or until + * finished or stopped. + * + * @param steps the number of steps to run for + */ + public void go(int steps); + + /** + * Start this watchable and run for the given amount of time, or until + * finished or stopped. + * + * @param millis the number of milliseconds to run for + */ + public void go(long millis); +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/action/GoToAction.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/action/GoToAction.java new file mode 100644 index 0000000000..82cd91116e --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/action/GoToAction.java @@ -0,0 +1,49 @@ +package com.github.librepdf.pdfrenderer.action; + +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFDestination; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/** + * An action which specifies going to a particular destination + */ +public class GoToAction extends PDFAction { + /** the destination to go to */ + private PDFDestination dest; + + /** + * Creates a new instance of GoToAction from an object + * + * @param obj the PDFObject with the action information + */ + public GoToAction(PDFObject obj, PDFObject root) throws IOException { + super("GoTo"); + + // find the destination + PDFObject destObj = obj.getDictRef("D"); + if (destObj == null) { + throw new PDFParseException("No destination in GoTo action " + obj); + } + + // parse it + this.dest = PDFDestination.getDestination(destObj, root); + } + + /** + * Create a new GoToAction from a destination + */ + public GoToAction(PDFDestination dest) { + super("GoTo"); + + this.dest = dest; + } + + /** + * Get the destination this action refers to + */ + public PDFDestination getDestination() { + return this.dest; + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/action/GoToEAction.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/action/GoToEAction.java new file mode 100644 index 0000000000..46c944845c --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/action/GoToEAction.java @@ -0,0 +1,269 @@ +package com.github.librepdf.pdfrenderer.action; + +import java.io.IOException; +import java.util.ArrayList; + +import com.github.librepdf.pdfrenderer.PDFDestination; +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/***************************************************************************** + * Action directing to a location within an embedded PDF document + * + * @author Katja Sondermann + * @since 07.07.2009 + ****************************************************************************/ +public class GoToEAction extends PDFAction { + + /** the destination within the remote PDF file */ + private PDFDestination destination; + /** the remote file this action refers to (optional)*/ + private String file = null; + /** Should the remote file be opened in a new window? (optional)*/ + private boolean newWindow = false; + /** The target dictionary*/ + private GoToETarget target; + + /** + * Creates a new instance of GoToEAction from an object + * @param obj the PDFObject with the action information + * @throws IOException - in case the action can not be parsed + */ + public GoToEAction(PDFObject obj, PDFObject root) throws IOException { + super("GoToE"); + // find the destination and parse it + this.destination = PdfObjectParseUtil.parseDestination("D", obj, root, true); + + // find the remote file and parse it + this.file = PdfObjectParseUtil.parseStringFromDict("F", obj, false); + + // find the new window attribute and parse it if available + this.newWindow = PdfObjectParseUtil.parseBooleanFromDict("NewWindow", obj, false); + + // parse the target dictionary + PDFObject targetObj = obj.getDictRef("T"); + ArrayList list = new ArrayList(); + this.target = parseTargetDistionary(targetObj, list); + } + + /************************************************************************* + * Parse a target dictionary if available + * @param targetObj + * @param list - a list of all already parsed targets, for not getting in an endless loop + * (if a target is found which is already contained, the recursive calling + * of this method will stop). + * @throws IOException - in case a value can not be parsed + ************************************************************************/ + private GoToETarget parseTargetDistionary(PDFObject targetObj, ArrayList list) throws IOException { + GoToETarget target = null; + if (targetObj != null) { + target = new GoToETarget(); + + // find the relation and parse it + target.setRelation(PdfObjectParseUtil.parseStringFromDict("R", targetObj, true)); + + // find the name of the embedded file and parse it + target.setNameInTree(PdfObjectParseUtil.parseStringFromDict("N", targetObj, false)); + + // find the page number and parse it + String page = PdfObjectParseUtil.parseStringFromDict("P", targetObj, false); + if(page == null){ + page = ""+PdfObjectParseUtil.parseIntegerFromDict("P", targetObj, false); + } + target.setPageNo(page); + + // find the annotation index and parse it + String annot = PdfObjectParseUtil.parseStringFromDict("A", targetObj, false); + if(annot == null){ + annot = ""+PdfObjectParseUtil.parseIntegerFromDict("A", targetObj, false); + } + target.setAnnotNo(annot); + + //find target dictionary and parse it + PDFObject subTargetObj = targetObj.getDictRef("T"); + if(subTargetObj != null){ + // call this method recursive, in case the target was not already contained in the + // list (this is checked for not getting into an infinite loop) + if(list.contains(target) == false){ + list.add(target); + GoToETarget subTargetDictionary = parseTargetDistionary(subTargetObj, list); + target.setTargetDictionary(subTargetDictionary); + } + } + } else { + if (this.file == null) { + throw new PDFParseException("No target dictionary in GoToE action " + targetObj); + } + } + return target; + } + + /************************************************************************* + * Create a new GoToEAction from the given attributes + * @param dest + * @param file + * @param newWindow + ************************************************************************/ + public GoToEAction(PDFDestination dest, String file, boolean newWindow) { + super("GoToR"); + this.file = file; + this.destination = dest; + this.newWindow = newWindow; + } + + /************************************************************************* + * Get the destination this action refers to + * @return PDFDestination + ************************************************************************/ + public PDFDestination getDestination() { + return this.destination; + } + + /************************************************************************* + * Get the file this action refers to + * @return PDFDestination + ************************************************************************/ + public String getFile() { + return this.file; + } + + /************************************************************************* + * Should the remote file be opened in a new window? + * @return boolean + ************************************************************************/ + public boolean isNewWindow() { + return this.newWindow; + } + + /************************************************************************* + * Get the target dictionary + * @return GoToETarget + ************************************************************************/ + public GoToETarget getTarget() { + return this.target; + } + + + /***************************************************************************** + * Inner class for holding the target dictionary's information + * + * @version $Id: GoToEAction.java,v 1.1 2009-07-10 12:47:31 xond Exp $ + * @author xond + * @since 07.07.2009 + ****************************************************************************/ + public static class GoToETarget { + private String relation; + private String nameInTree; + private String pageNo; + private String annotNo; + private GoToETarget targetDictionary; + + /************************************************************************* + * Relation between current document and the target. Can either be "P" or "C" + * @return String + ************************************************************************/ + public String getRelation() { + return this.relation; + } + + /************************************************************************* + * Relation between current document and the target. Can either be "P" or "C" + * @param relation + ************************************************************************/ + public void setRelation(String relation) { + this.relation = relation; + } + + /************************************************************************* + * The file name in the embedded files tree + * @return String + ************************************************************************/ + public String getNameInTree() { + return this.nameInTree; + } + + /************************************************************************* + * The file name in the embedded files tree + * @param nameInTree + ************************************************************************/ + public void setNameInTree(String nameInTree) { + this.nameInTree = nameInTree; + } + + /************************************************************************* + * Page Number: + * If the value can be parsed as Integer, it specifies the page number in + * the current document containing the file attachment annotation. If the + * value is a string, it defines a named destination in the current document + * that provides the page number of the file attachment annotation. + * + * @return String + ************************************************************************/ + public String getPageNo() { + return this.pageNo; + } + + /************************************************************************* + * Page Number: + * If the value can be parsed as Integer, it specifies the page number in + * the current document containing the file attachment annotation. If the + * value is a string, it defines a named destination in the current document + * that provides the page number of the file attachment annotation. + * + * @param pageNo + ************************************************************************/ + public void setPageNo(String pageNo) { + this.pageNo = pageNo; + } + + /************************************************************************* + * The index of the according annotation in the annotations array + * @return String + ************************************************************************/ + public String getAnnotNo() { + return this.annotNo; + } + + /************************************************************************* + * The index of the according annotation in the annotations array + * @param annotNo + ************************************************************************/ + public void setAnnotNo(String annotNo) { + this.annotNo = annotNo; + } + + /************************************************************************* + * A target dictionary specifying additional target information. If missing, + * the current document is the target file containing the destination. + * @return GoToETarget + ************************************************************************/ + public GoToETarget getTargetDictionary() { + return this.targetDictionary; + } + + /************************************************************************* + * A target dictionary specifying additional target information. If missing, + * the current document is the target file containing the destination. + * @param targetDictionary + ************************************************************************/ + public void setTargetDictionary(GoToETarget targetDictionary) { + this.targetDictionary = targetDictionary; + } + + @Override + public boolean equals(Object obj) { + if((obj instanceof GoToETarget) == false){ + return false; + } + if(super.equals(obj)){ + return true; + } + GoToETarget that = (GoToETarget)obj; + // compare the strng values, as the attributes may also be null + return String.valueOf(this.annotNo).equals(String.valueOf(that.annotNo)) + && String.valueOf(this.nameInTree).equals(String.valueOf(that.nameInTree)) + && String.valueOf(this.pageNo).equals(String.valueOf(that.pageNo)) + && String.valueOf(this.relation).equals(String.valueOf(that.relation)); + } + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/action/GoToRAction.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/action/GoToRAction.java new file mode 100644 index 0000000000..515db6bfbc --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/action/GoToRAction.java @@ -0,0 +1,75 @@ +package com.github.librepdf.pdfrenderer.action; + +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.PDFDestination; +import com.github.librepdf.pdfrenderer.PDFObject; + +/***************************************************************************** + * Action directing to a location within another PDF document + * + * @author Katja Sondermann + * @since 07.07.2009 + ****************************************************************************/ +public class GoToRAction extends PDFAction { + + /** the destination within the remote PDF file */ + private PDFDestination destination; + /** the remote file this action refers to*/ + private String file; + /** Should the remote file be opened in a new window? (optional)*/ + private boolean newWindow=false; + /** + * Creates a new instance of GoToRAction from an object + * @param obj the PDFObject with the action information + * @throws IOException - in case the action can not be parsed + */ + public GoToRAction(PDFObject obj, PDFObject root) throws IOException { + super("GoToR"); + // find the destination and parse it + this.destination = PdfObjectParseUtil.parseDestination("D", obj, root, true); + + // find the remote file and parse it + this.file = PdfObjectParseUtil.parseStringFromDict("F", obj, true); + + // find the new window attribute and parse it if available + this.newWindow = PdfObjectParseUtil.parseBooleanFromDict("NewWindow", obj, false); + } + + /************************************************************************* + * Create a new GoToRAction from the given attributes + * @param dest + * @param file + * @param newWindow + ************************************************************************/ + public GoToRAction(PDFDestination dest, String file, boolean newWindow){ + super("GoToR"); + this.file = file; + this.destination = dest; + this.newWindow = newWindow; + } + + /************************************************************************* + * Get the destination this action refers to + * @return PDFDestination + ************************************************************************/ + public PDFDestination getDestination() { + return this.destination; + } + + /************************************************************************* + * Get the file this action refers to + * @return PDFDestination + ************************************************************************/ + public String getFile() { + return this.file; + } + + /************************************************************************* + * Should the remote file be opened in a new window? + * @return boolean + ************************************************************************/ + public boolean isNewWindow() { + return this.newWindow; + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/action/LaunchAction.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/action/LaunchAction.java new file mode 100644 index 0000000000..ccf928bfd5 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/action/LaunchAction.java @@ -0,0 +1,492 @@ +package com.github.librepdf.pdfrenderer.action; + +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/***************************************************************************** + * Action for launching an application, mostly used to open a file. + * + * @author Katja Sondermann + * @since 08.07.2009 + ****************************************************************************/ +public class LaunchAction extends PDFAction { + // file separator according to PDF spec + public final static String SOLIDUS = "/"; + + /** the file/application to be opened (optional)*/ + private FileSpec file; + /** should a new window be opened (optional)*/ + private boolean newWindow = false; + private PDFObject unixParam; + private PDFObject macParam; + private WinLaunchParam winParam; + + /** + * Creates a new instance of LaunchAction from an object + * + * @param obj - the PDFObject with the action information + * @param root - the root object + */ + public LaunchAction(PDFObject obj, PDFObject root) throws IOException { + super("Launch"); + // find the file/application and parse it + PDFObject fileObj = obj.getDictRef("F"); + this.file = parseFileSpecification(fileObj); + + // find the new window flag and parse it + PDFObject newWinObj = obj.getDictRef("NewWindow"); + if (newWinObj != null) { + this.newWindow = newWinObj.getBooleanValue(); + } + // parse the OS specific launch parameters: + this.winParam = parseWinDict(obj.getDictRef("Win")); + // unix and mac dictionaries are not further specified, so can not be parsed yet. + this.unixParam = obj.getDictRef("Unix"); + this.macParam = obj.getDictRef("Mac"); + + // check if at least the file or one of the OS specific launch parameters is set: + if ((this.file == null) + && (this.winParam == null) + && (this.unixParam == null) + && (this.macParam == null)) { + throw new PDFParseException("Could not parse launch action (file or OS " + + "specific launch parameters are missing): " + obj.toString()); + } + } + + /************************************************************************* + * Is the file name absolute (if not, it is relative to the path of the + * currently opened PDF file). + * If the file name starts with a "/", it is considered to be absolute. + * + * @return boolean + ************************************************************************/ + public static boolean isAbsolute(String fileName) { + return fileName.startsWith(SOLIDUS); + } + + /************************************************************************* + * Parse the file specification object + * @param fileObj + * @return FileSpec - might be null in case the passed object is null + * @throws IOException + * @throws PDFParseException + ************************************************************************/ + private FileSpec parseFileSpecification(PDFObject fileObj) throws PDFParseException, IOException { + FileSpec file = null; + if (fileObj != null) { + file = new FileSpec(); + if(fileObj.getType() == PDFObject.DICTIONARY){ + file.setFileSystem(PdfObjectParseUtil.parseStringFromDict("FS", fileObj, false)); + file.setFileName(PdfObjectParseUtil.parseStringFromDict("F", fileObj, false)); + file.setUnicode(PdfObjectParseUtil.parseStringFromDict("UF", fileObj, false)); + file.setDosFileName(PdfObjectParseUtil.parseStringFromDict("DOS", fileObj, false)); + file.setMacFileName(PdfObjectParseUtil.parseStringFromDict("Mac", fileObj, false)); + file.setUnixFileName(PdfObjectParseUtil.parseStringFromDict("Unix", fileObj, false)); + file.setVolatileFile(PdfObjectParseUtil.parseBooleanFromDict("V", fileObj, false)); + file.setDescription(PdfObjectParseUtil.parseStringFromDict("Desc", fileObj, false)); + file.setId(fileObj.getDictRef("ID")); + file.setEmbeddedFile(fileObj.getDictRef("EF")); + file.setRelatedFile(fileObj.getDictRef("RF")); + file.setCollectionItem(fileObj.getDictRef("CI")); + }else if(fileObj.getType() == PDFObject.STRING){ + file.setFileName(fileObj.getStringValue()); + }else{ + throw new PDFParseException("File specification could not be parsed " + + "(should be of type 'Dictionary' or 'String'): " + fileObj.toString()); + } + } + return file; + } + + + /************************************************************************* + * Parse the windows specific launch parameters + * @param winDict + * @throws IOException - in case of a problem during parsing content + ************************************************************************/ + private WinLaunchParam parseWinDict(PDFObject winDict) throws IOException { + if (winDict == null) { + return null; + } + WinLaunchParam param = new WinLaunchParam(); + + // find and parse the file/application name + param.setFileName(PdfObjectParseUtil.parseStringFromDict("F", winDict, true)); + + // find and parse the directory + param.setDirectory(PdfObjectParseUtil.parseStringFromDict("D", winDict, false)); + + // find and parse the operation to be performed + param.setOperation(PdfObjectParseUtil.parseStringFromDict("O", winDict, false)); + + // find and parse the parameter to be passed to the application + param.setParameter(PdfObjectParseUtil.parseStringFromDict("P", winDict, false)); + + return param; + } + + /************************************************************************* + * The file / application to be opened + * @return FileSpec + ************************************************************************/ + public FileSpec getFileSpecification() { + return this.file; + } + + /************************************************************************* + * Should a new window be opened for the file/application? + * @return boolean + ************************************************************************/ + public boolean isNewWindow() { + return this.newWindow; + } + + /************************************************************************* + * Get the unix specific launch parameters. + * Note: The dictionary is not specified yet in the PDF spec., so the PdfObject + * which is returned here is not parsed. + * @return PDFObject + ************************************************************************/ + public PDFObject getUnixParam() { + return this.unixParam; + } + + /************************************************************************* + * Get the mac specific launch parameters. + * Note: The dictionary is not specified yet in the PDF spec., so the PdfObject + * which is returned here is not parsed. + * @return PDFObject + ************************************************************************/ + public PDFObject getMacParam() { + return this.macParam; + } + + /************************************************************************* + * Get the windows specific launch parameters. + * @return WinLaunchParam + ************************************************************************/ + public WinLaunchParam getWinParam() { + return this.winParam; + } + + /***************************************************************************** + * Internal class for the windows specific launch parameters + * + * @version $Id: LaunchAction.java,v 1.1 2009-07-10 12:47:31 xond Exp $ + * @author xond + * @since 08.07.2009 + ****************************************************************************/ + public class WinLaunchParam { + private String fileName; + private String directory; + private String operation = "open"; + private String parameter; + + /************************************************************************* + * The file/application name to be opened + * @return String + ************************************************************************/ + public String getFileName() { + return this.fileName; + } + + /************************************************************************* + * The file/application name to be opened + * @param fileName + ************************************************************************/ + public void setFileName(String fileName) { + this.fileName = fileName; + } + + /************************************************************************* + * The directory in standard DOS syntax + * @return String + ************************************************************************/ + public String getDirectory() { + return this.directory; + } + + /************************************************************************* + * The directory in standard DOS syntax + * @param directory + ************************************************************************/ + public void setDirectory(String directory) { + this.directory = directory; + } + + /************************************************************************* + * The operation to be performed (open or print). Ignored + * in case the "F" parameter describes a file to be opened. + * Default is "open". + * @return String + ************************************************************************/ + public String getOperation() { + return this.operation; + } + + /************************************************************************* + * The operation to be performed ("open" or "print").Ignored + * in case the "F" parameter describes a file to be opened. + * Default is "open". + * @param operation + ************************************************************************/ + public void setOperation(String operation) { + this.operation = operation; + } + + /************************************************************************* + * A parameter which shall be passed to the application. Ignored + * in case the "F" parameter describes a file to be opened. + * @return String + ************************************************************************/ + public String getParameter() { + return this.parameter; + } + + /************************************************************************* + * A parameter which shall be passed to the application. Ignored + * in case the "F" parameter describes a file to be opened. + * @param parameter + ************************************************************************/ + public void setParameter(String parameter) { + this.parameter = parameter; + } + } + + /***************************************************************************** + * Inner class for storing a file specification + * + * @version $Id: LaunchAction.java,v 1.1 2009-07-10 12:47:31 xond Exp $ + * @author xond + * @since 08.07.2009 + ****************************************************************************/ + public static class FileSpec{ + private String fileSystem; + private String fileName; + private String dosFileName; + private String unixFileName; + private String macFileName; + private String unicode; + private PDFObject id; + private boolean volatileFile; + private PDFObject embeddedFile; + private PDFObject relatedFile; + private String description; + private PDFObject collectionItem; + + /************************************************************************* + * The name of the file system that should be used to interpret this entry. + * @return String + ************************************************************************/ + public String getFileSystem() { + return this.fileSystem; + } + + /************************************************************************* + * The name of the file system that should be used to interpret this entry. + * @param fileSystem + ************************************************************************/ + public void setFileSystem(String fileSystem) { + this.fileSystem = fileSystem; + } + + /************************************************************************* + * Get the filename: + * first try to get the file name for the used OS, if it's not available + * return the common file name. + * @return String + ************************************************************************/ + public String getFileName() { + String system = System.getProperty("os.name"); + if(system.startsWith("Windows")){ + if(this.dosFileName != null){ + return this.dosFileName; + } + }else if(system.startsWith("mac os x")){ + if(this.macFileName != null){ + return this.macFileName; + } + }else { + if(this.unixFileName != null){ + return this.unixFileName; + } + } + return this.fileName; + } + + /************************************************************************* + * The file name. + * @param fileName + ************************************************************************/ + public void setFileName(String fileName) { + this.fileName = fileName; + } + + /************************************************************************* + * A file specification string representing a DOS file name. + * @return String + ************************************************************************/ + public String getDosFileName() { + return this.dosFileName; + } + + /************************************************************************* + * A file specification string representing a DOS file name. + * @param dosFileName + ************************************************************************/ + public void setDosFileName(String dosFileName) { + this.dosFileName = dosFileName; + } + + /************************************************************************* + * A file specification string representing a unix file name. + * @return String + ************************************************************************/ + public String getUnixFileName() { + return this.unixFileName; + } + + /************************************************************************* + * A file specification string representing a unix file name. + * @param unixFileName + ************************************************************************/ + public void setUnixFileName(String unixFileName) { + this.unixFileName = unixFileName; + } + + /************************************************************************* + * A file specification string representing a mac file name. + * @return String + ************************************************************************/ + public String getMacFileName() { + return this.macFileName; + } + + /************************************************************************* + * A file specification string representing a mac file name. + * @param macFileName + ************************************************************************/ + public void setMacFileName(String macFileName) { + this.macFileName = macFileName; + } + + /************************************************************************* + * Unicode file name + * @return String + ************************************************************************/ + public String getUnicode() { + return this.unicode; + } + + /************************************************************************* + * Unicode file name + * @param unicode + ************************************************************************/ + public void setUnicode(String unicode) { + this.unicode = unicode; + } + + /************************************************************************* + * ID - array of two byte strings constituting a file identifier, which + * should be included in the referenced file. + * + * @return PDFObject + ************************************************************************/ + public PDFObject getId() { + return this.id; + } + + /************************************************************************* + * ID - array of two byte strings constituting a file identifier, which + * should be included in the referenced file. + * + * @param id + ************************************************************************/ + public void setId(PDFObject id) { + this.id = id; + } + + /************************************************************************* + * Is the file volatile? + * @return boolean + ************************************************************************/ + public boolean isVolatileFile() { + return this.volatileFile; + } + + /************************************************************************* + * Is the file volatile? + * @param volatileFile + ************************************************************************/ + public void setVolatileFile(boolean volatileFile) { + this.volatileFile = volatileFile; + } + + /************************************************************************* + * Dictionary of embedded file streams + * @return PDFObject + ************************************************************************/ + public PDFObject getEmbeddedFile() { + return this.embeddedFile; + } + + /************************************************************************* + * Dictionary of embedded file streams + * @param embeddedFile + ************************************************************************/ + public void setEmbeddedFile(PDFObject embeddedFile) { + this.embeddedFile = embeddedFile; + } + + /************************************************************************* + * Dictionary of related files. + * @return PDFObject + ************************************************************************/ + public PDFObject getRelatedFile() { + return this.relatedFile; + } + + /************************************************************************* + * Dictionary of related files. + * @param relatedFile + ************************************************************************/ + public void setRelatedFile(PDFObject relatedFile) { + this.relatedFile = relatedFile; + } + + /************************************************************************* + * File specification description + * @return String + ************************************************************************/ + public String getDescription() { + return this.description; + } + + /************************************************************************* + * File specification description + * @param description + ************************************************************************/ + public void setDescription(String description) { + this.description = description; + } + + /************************************************************************* + * Collection item dictionary + * @return PDFObject + ************************************************************************/ + public PDFObject getCollectionItem() { + return this.collectionItem; + } + + /************************************************************************* + * Collection item dictionary + * @param collectionItem + ************************************************************************/ + public void setCollectionItem(PDFObject collectionItem) { + this.collectionItem = collectionItem; + } + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/action/PDFAction.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/action/PDFAction.java new file mode 100644 index 0000000000..6fdd010645 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/action/PDFAction.java @@ -0,0 +1,107 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.action; + +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/** + * The common super-class of all PDF actions. + */ +public class PDFAction { + /** the type of this action */ + private String type; + + /** the next action or array of actions */ + private PDFObject next; + + /** Creates a new instance of PDFAction */ + public PDFAction(String type) { + this.type = type; + } + + /** + * Get an action of the appropriate type from a PDFObject + * + * @param obj the PDF object containing the action to parse + * @param root the root of the PDF object tree + */ + public static PDFAction getAction(PDFObject obj, PDFObject root) + throws IOException + { + // figure out the action type + PDFObject typeObj = obj.getDictRef("S"); + if (typeObj == null) { + throw new PDFParseException("No action type in object: " + obj); + } + + // create the action based on the type + PDFAction action = null; + String type = typeObj.getStringValue(); + if (type.equals("GoTo")) { + action = new GoToAction(obj, root); + }else if(type.equals("GoToE")){ + action = new GoToEAction(obj, root); + }else if(type.equals("GoToR")){ + action = new GoToRAction(obj, root); + }else if(type.equals("URI")){ + action = new UriAction(obj, root); + }else if(type.equals("Launch")){ + action = new LaunchAction(obj, root); + } + else { + /** [JK FIXME: Implement other action types! ] */ + throw new PDFParseException("Unknown Action type: " + type); + } + + // figure out if there is a next action + PDFObject nextObj = obj.getDictRef("Next"); + if (nextObj != null) { + action.setNext(nextObj); + } + + // return the action + return action; + } + + /** + * Get the type of this action + */ + public String getType() { + return this.type; + } + + /** + * Get the next action or array of actions + */ + public PDFObject getNext() { + return this.next; + } + + /** + * Set the next action or array of actions + */ + public void setNext(PDFObject next) { + this.next = next; + } + +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/action/PdfObjectParseUtil.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/action/PdfObjectParseUtil.java new file mode 100644 index 0000000000..3a51e8cb50 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/action/PdfObjectParseUtil.java @@ -0,0 +1,100 @@ +package com.github.librepdf.pdfrenderer.action; + +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.PDFDestination; +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/***************************************************************************** + * Utility class for parsing values from a PDFObject + * + * @author Katja Sondermann + * @since 08.07.2009 + ****************************************************************************/ +public class PdfObjectParseUtil { + + /************************************************************************* + * Parse a String value with the given key from parent object. If it's mandatory + * and not available, an exception will be thrown. + * @param key + * @param parent + * @param mandatory + * @return String - can be null if not mandatory + * @throws IOException - in case of a parsing error + ************************************************************************/ + public static String parseStringFromDict(String key, PDFObject parent, boolean mandatory) throws IOException{ + PDFObject val = parent; + while (val.getType() == PDFObject.DICTIONARY) { + val = val.getDictRef(key); + if(val == null){ + if(mandatory){ + throw new PDFParseException(key + "value could not be parsed : " + parent.toString()); + } + return null; + } + } + return val.getStringValue(); + } + + /************************************************************************* + * Parse a Boolean value with the given key from parent object. If it's mandatory + * and not available, an exception will be thrown. + * @param key + * @param parent + * @param mandatory + * @return boolean - false if not available and not mandatory + * @throws IOException + ************************************************************************/ + public static boolean parseBooleanFromDict(String key, PDFObject parent, boolean mandatory) throws IOException{ + PDFObject val = parent.getDictRef(key); + if(val == null){ + if(mandatory){ + throw new PDFParseException(key + "value could not be parsed : " + parent.toString()); + } + return false; + } + return val.getBooleanValue(); + } + + /************************************************************************* + * Parse a integer value with the given key from parent object. If it's mandatory + * and not available, an exception will be thrown. + * @param key + * @param parent + * @param mandatory + * @return int - returns "0" in case the value is not a number + * @throws IOException + ************************************************************************/ + public static int parseIntegerFromDict(String key, PDFObject parent, boolean mandatory) throws IOException{ + PDFObject val = parent.getDictRef(key); + if(val == null){ + if(mandatory){ + throw new PDFParseException(key + "value could not be parsed : " + parent.toString()); + } + return 0; + } + return val.getIntValue(); + } + + /************************************************************************* + * Parse a destination object + * @param key + * @param parent + * @param root + * @param mandatory + * @return PDFDestination - can be null if not mandatory + * @throws IOException + ************************************************************************/ + public static PDFDestination parseDestination(String key, PDFObject parent, PDFObject root, boolean mandatory) throws IOException{ + PDFObject destObj = parent.getDictRef(key); + if (destObj == null) { + if(mandatory){ + throw new PDFParseException("Error parsing destination " + parent); + } + return null; + } + return PDFDestination.getDestination(destObj, root); + + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/action/UriAction.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/action/UriAction.java new file mode 100644 index 0000000000..6786da9509 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/action/UriAction.java @@ -0,0 +1,45 @@ +package com.github.librepdf.pdfrenderer.action; + +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.PDFObject; + +/***************************************************************************** + * URI action, containing a web link + * + * @author Katja Sondermann + * @since 07.07.2009 + ****************************************************************************/ +public class UriAction extends PDFAction { + + /** The URL this action links to */ + private String uri; + + /************************************************************************* + * Constructor, reading the URL from the given action object + * @param type + * @throws IOException - in case the action can not be parsed + ************************************************************************/ + public UriAction(PDFObject obj, PDFObject root) throws IOException { + super("URI"); + this.uri = PdfObjectParseUtil.parseStringFromDict("URI", obj, true); + } + + /************************************************************************* + * Constructor + * @param type + * @throws IOException + ************************************************************************/ + public UriAction(String uri) throws IOException { + super("URI"); + this.uri = uri; + } + + /************************************************************************* + * Get the URI this action directs to + * @return String + ************************************************************************/ + public String getUri() { + return this.uri; + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/AnnotationBorderStyle.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/AnnotationBorderStyle.java new file mode 100644 index 0000000000..93da4ba92c --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/AnnotationBorderStyle.java @@ -0,0 +1,88 @@ +package com.github.librepdf.pdfrenderer.annotation; + +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.PDFObject; + +/** + * Annotation border style + * @author Bernd Rosstauscher + */ +public class AnnotationBorderStyle { + + public enum BorderStyle { + SOLID("S"), + DASHED("D"), + BEVELED("B"), + INSET("I"), + UNDERLINE("U"); + + private String code; + + private BorderStyle(String code) { + this.code = code; + } + + public String getCode() { + return code; + } + + public static BorderStyle fromCode(String code) { + for (BorderStyle bs : BorderStyle.values()) { + if (bs.getCode().equals(code)) { + return bs; + } + } + return SOLID; + } + + } + + private Integer width; + private BorderStyle borderStyle; + private int[] dashArray; + + /** + * Creates a annotation border style + */ + public AnnotationBorderStyle() { + super(); + } + + /** + * Parse a border style from a BS dictionary. + * @param bs the pdf dictionary to parse. + * @return the border style object. + * @throws IOException + */ + public static AnnotationBorderStyle parseFromDictionary(PDFObject bs) throws IOException { + AnnotationBorderStyle result = new AnnotationBorderStyle(); + result.width = bs.getDictRefAsInt("W"); + result.borderStyle = BorderStyle.fromCode(bs.getDictRefAsString("S")); + result.dashArray = bs.getDictRefAsIntArray("D"); + // TODO BE Border effect not supported yet + return result; + } + + /** + * @return the border style enum + */ + public BorderStyle getBorderStyle() { + return borderStyle; + } + + /** + * @return the width of the border line + */ + public Integer getWidth() { + return width; + } + + /** + * @return specifying the dash for the border line + */ + public int[] getDashArray() { + return dashArray; + } + +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/AnnotationType.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/AnnotationType.java new file mode 100644 index 0000000000..97eaf95715 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/AnnotationType.java @@ -0,0 +1,106 @@ +package com.github.librepdf.pdfrenderer.annotation; + +import com.github.librepdf.pdfrenderer.Configuration; + +/** + * @author Bernd Rosstauscher + * + */ +public enum AnnotationType{ + UNKNOWN("-", 0, PDFAnnotation.class), + LINK("Link", 1, LinkAnnotation.class), + WIDGET("Widget", 2, WidgetAnnotation.class), + STAMP("Stamp", 3, StampAnnotation.class), + FREETEXT("FreeText", 5, FreetextAnnotation.class), + SIGNATURE("Sig", 6, WidgetAnnotation.class), + + // added more annotation types. Most of them only with basic features + // We render them all via the base class MarkupAnnotation + + TEXT("Text", 7, MarkupAnnotation.class), + LINE("Line", 8, MarkupAnnotation.class), + SQUARE("Square", 9, SquareAnnotation.class), + CIRCLE("Circle", 10, CircleAnnotation.class), + POLYGON("Polygon", 11, MarkupAnnotation.class), + POLYLINE("PolyLine", 12, MarkupAnnotation.class), + HIGHLIGHT("Highlight", 13, TextMarkupAnnotation.class), + UNDERLINE("Underline", 14, TextMarkupAnnotation.class), + SQUIGGLY("Squiggly", 15, TextMarkupAnnotation.class), + STRIKEOUT("StrikeOut", 16, TextMarkupAnnotation.class), + CARET("Caret", 17, MarkupAnnotation.class), + INK("Ink", 18, MarkupAnnotation.class), + //POPUP("Popup", 19, MarkupAnnotation.class), + FILEATTACHMENT("FileAttachment", 20, PDFAnnotation.class), + SOUND("Sound", 21, PDFAnnotation.class), + MOVIE("Movie", 22, PDFAnnotation.class), + SCREEN("Screen", 23, PDFAnnotation.class), + PRINTERMARK("PrinterMark", 24, PDFAnnotation.class), + TRAPNET("TrapNet", 25, PDFAnnotation.class), + WATERMARK("Watermark", 26, PDFAnnotation.class), + THREED("3D", 27, PDFAnnotation.class), + REDACT("Redact", 28, MarkupAnnotation.class), + ; + + /** + * @return true if this annotation type should be displayed else false. + */ + boolean displayAnnotation() { + switch(this) { + case STAMP: return Configuration.getInstance().isPrintStampAnnotations(); + case WIDGET: return Configuration.getInstance().isPrintWidgetAnnotations(); + case FREETEXT: return Configuration.getInstance().isPrintFreetextAnnotations(); + case LINK: return Configuration.getInstance().isPrintLinkAnnotations(); + case SIGNATURE: return Configuration.getInstance().isPrintSignatureFields(); + case UNKNOWN: return false; + default: { + // Fallback for all the annotation types that are currently mapped to MarkupAnnotation + return MarkupAnnotation.class.isAssignableFrom(this.className) + && Configuration.getInstance().isPrintFreetextAnnotations(); + } + } + } + + private String definition; + private int internalId; + private Class className; + + private AnnotationType(String definition, int typeId, Class className) { + this.definition = definition; + this.internalId = typeId; + this.className = className; + } + + /** + * @return the definition + */ + public String getDefinition() { + return definition; + } + /** + * @return the internalId + */ + public int getInternalId() { + return internalId; + } + + /** + * @return the className + */ + public Class getClassName() { + return className; + } + + /** + * Get annotation type by it's type + * @param definition + * @return + */ + public static AnnotationType getByDefinition(String definition) { + for (AnnotationType type : values()) { + if(type.definition.equals(definition)) { + return type; + } + } + return UNKNOWN; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/CircleAnnotation.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/CircleAnnotation.java new file mode 100644 index 0000000000..df48b8cd28 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/CircleAnnotation.java @@ -0,0 +1,26 @@ +package com.github.librepdf.pdfrenderer.annotation; + +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.PDFObject; + +/***************************************************************************** + * PDF annotation for a circle + * + * @author Bernd Rosstauscher + ****************************************************************************/ +public class CircleAnnotation extends MarkupAnnotation { + + // TODO Not all of this is fully implemented yet. + // But it will work if the visual representation is done via an "Appearance Stream" + + /************************************************************************* + * Constructor + * @param annotObject + * @throws IOException + ************************************************************************/ + public CircleAnnotation(PDFObject annotObject) throws IOException { + super(annotObject, AnnotationType.CIRCLE); + + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/FreetextAnnotation.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/FreetextAnnotation.java new file mode 100644 index 0000000000..d96cd18c67 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/FreetextAnnotation.java @@ -0,0 +1,24 @@ +package com.github.librepdf.pdfrenderer.annotation; + +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.PDFObject; + +/***************************************************************************** + * PDF annotation describing a free text + * Currently only supports the XObjects which can be found in the path AP->N + * of the annotation object (same implementation as the stamp annotation) + * @author Katja Sondermann + * @since 28.03.2012 + ****************************************************************************/ +public class FreetextAnnotation extends MarkupAnnotation { + + /************************************************************************* + * Constructor + * @param annotObject + * @throws IOException + ************************************************************************/ + public FreetextAnnotation(PDFObject annotObject) throws IOException { + super(annotObject, AnnotationType.FREETEXT); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/LinkAnnotation.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/LinkAnnotation.java new file mode 100644 index 0000000000..10a31ac299 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/LinkAnnotation.java @@ -0,0 +1,67 @@ +package com.github.librepdf.pdfrenderer.annotation; + +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.PDFDestination; +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; +import com.github.librepdf.pdfrenderer.action.GoToAction; +import com.github.librepdf.pdfrenderer.action.PDFAction; + +/***************************************************************************** + * PDF annotation describing a link to either a location within the current + * document, a location in another PDF file, an application/file to be opened + * or a web site. + * In the PDF structure a link can be a destination ("DEST") or an action ("A"). + * Both ways are handled as actions internally, i.e. for getting the links + * destination, you should get the action from this annotation object. It can be + * one of the following actions: + *

  • GotoAction - for a file internal destination
  • + *
  • GoToRAction - for a destination in a remote PDF file
  • + *
  • GoToEAction - for a destination in an embedded PDF file
  • + *
  • UriAction - for a web link
  • + *
  • LaunchAction - for launching an application/opening a file
  • + * + * @author Katja Sondermann + * @since 06.07.2009 + ****************************************************************************/ +public class LinkAnnotation extends PDFAnnotation { + + private PDFAction action = null; + + /************************************************************************* + * Constructor + * @param annotObject + * @throws IOException + ************************************************************************/ + public LinkAnnotation(PDFObject annotObject) throws IOException { + super(annotObject, AnnotationType.LINK); + // a link annotation can either have an action (GoTo or URI) or a destination (DEST) + PDFObject actionObj = annotObject.getDictRef("A"); + if (actionObj != null) { + this.action = PDFAction.getAction(actionObj, annotObject.getRoot()); + } else { + // if a destination is given, create a GoToAction from it + PDFObject dest = annotObject.getDictRef("Dest"); + if(dest == null) { + dest = annotObject.getDictRef("DEST"); + } + if (dest != null) { + this.action = new GoToAction(PDFDestination.getDestination(dest, annotObject.getRoot())); + } else { + throw new PDFParseException( + "Could not parse link annotation (no Action or Destination found): " + + annotObject.toString()); + } + } + } + + /************************************************************************* + * Get the contained PDFAction + * @return PDFAction - can be null in case the contains + * a destination object + ************************************************************************/ + public PDFAction getAction() { + return this.action; + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/MarkupAnnotation.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/MarkupAnnotation.java new file mode 100644 index 0000000000..fecb96abe2 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/MarkupAnnotation.java @@ -0,0 +1,257 @@ +package com.github.librepdf.pdfrenderer.annotation; + +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.awt.geom.Rectangle2D.Float; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import com.github.librepdf.pdfrenderer.PDFCmd; +import com.github.librepdf.pdfrenderer.PDFImage; +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFPage; +import com.github.librepdf.pdfrenderer.PDFParseException; +import com.github.librepdf.pdfrenderer.PDFParser; + +/***************************************************************************** + * PDF annotation describing a all kind of "markup" annotations which are visible + * in the PDF. + * + * @author Bernd Rosstauscher + ****************************************************************************/ +public class MarkupAnnotation extends PDFAnnotation { + + private PDFObject onAppearance; + private PDFObject offAppearance; + private List onCmd; + private List offCmd; + private boolean appearanceStateOn; + private AnnotationBorderStyle borderStyle; + private String textLabel; + private PDFAnnotation popupAnnotation; + + + /************************************************************************* + * Constructor + * @param annotObject + * @throws IOException + ************************************************************************/ + + public MarkupAnnotation(PDFObject annotObject, AnnotationType type) throws IOException { + super(annotObject, type); + + this.textLabel = annotObject.getDictRefAsString("T"); + // TODO more is missing here like CA, RC, ... + + parsePopupAnnotation(annotObject.getDictRef("Popup")); + parseAP(annotObject.getDictRef("AP")); + parseBorderStyleDictionary(annotObject.getDictRef("BS")); + } + + /** + * Parses the appearance stream into PDF commands + * @param dictRef + * @throws IOException + */ + protected void parseAP(PDFObject dictRef) throws IOException { + if(dictRef == null) { + return; + } + PDFObject normalAP = dictRef.getDictRef("N"); + if(normalAP == null) { + return; + } + if(normalAP.getType() == PDFObject.DICTIONARY) { + this.onAppearance = normalAP.getDictRef("On"); + this.offAppearance = normalAP.getDictRef("Off"); + PDFObject as = dictRef.getDictRef("AS"); + this.appearanceStateOn = (as != null) && ("On".equals(as.getStringValue())); + }else { + this.onAppearance = normalAP; + this.offAppearance = null; + appearanceStateOn = true; + } + parseOnOffCommands(); + } + + /** + * Parses the mouse On or Off appearance stream + * depending on which one is currently active. + * @throws IOException + */ + private void parseOnOffCommands() throws IOException { + if(onAppearance != null) { + onCmd = parseIntoPdfCommands(onAppearance); + } + if(offAppearance != null) { + offCmd = parseIntoPdfCommands(offAppearance); + } + } + + /** + * Parses the border style dictionary + * @param bs + * @throws IOException + */ + protected void parseBorderStyleDictionary(PDFObject bs) throws IOException { + if (bs != null) { + this.borderStyle = AnnotationBorderStyle.parseFromDictionary(bs); + } + } + + /** + * @return the border style or null if not specified. + */ + public AnnotationBorderStyle getBorderStyle() { + return borderStyle; + } + + /** + * Parses the popup annotation + * @param popupObj + * @throws IOException + */ + private void parsePopupAnnotation(PDFObject popupObj) throws IOException { + this.popupAnnotation = (popupObj != null)?createAnnotation(popupObj):null; + } + + + private List parseIntoPdfCommands(PDFObject obj) throws IOException { + // TODO see also WidgetAnnotation.parseCommand which seems to be copied code + // We should merge these two + String type = obj.getDictRef("Subtype").getStringValue(); + if (type == null) { + type = obj.getDictRef ("S").getStringValue (); + } + ArrayList result = new ArrayList(); + result.add(PDFPage.createPushCmd()); + result.add(PDFPage.createPushCmd()); + if (type.equals("Image")) { + // stamp annotation transformation + AffineTransform rectAt = getPositionTransformation(); + result.add(PDFPage.createXFormCmd(rectAt)); + + PDFImage img = PDFImage.createImage(obj, new HashMap() , false); + result.add(PDFPage.createImageCmd(img)); + } else if (type.equals("Form")) { + + // rats. parse it. + PDFObject bobj = obj.getDictRef("BBox"); + float xMin = bobj.getAt(0).getFloatValue(); + float yMin = bobj.getAt(1).getFloatValue(); + float xMax = bobj.getAt(2).getFloatValue(); + float yMax = bobj.getAt(3).getFloatValue(); + Float bbox = new Rectangle2D.Float(xMin, + yMin, + xMax - xMin, + yMax - yMin); + PDFPage formCmds = new PDFPage(bbox, 0); + + // stamp annotation transformation + AffineTransform rectAt = getPositionTransformation(); + formCmds.addXform(rectAt); + + AffineTransform rectScaled = getScalingTransformation(bbox); + formCmds.addXform(rectScaled); + + // form transformation + AffineTransform at; + PDFObject matrix = obj.getDictRef("Matrix"); + if (matrix == null) { + at = new AffineTransform(); + } else { + float elts[] = new float[6]; + for (int i = 0; i < elts.length; i++) { + elts[i] = (matrix.getAt(i)).getFloatValue(); + } + at = new AffineTransform(elts); + } + formCmds.addXform(at); + + HashMap r = new HashMap(new HashMap()); + PDFObject rsrc = obj.getDictRef("Resources"); + if (rsrc != null) { + r.putAll(rsrc.getDictionary()); + } + + PDFParser form = new PDFParser(formCmds, obj.getStream(), r); + form.go(true); + + result.addAll(formCmds.getCommands()); + } else { + throw new PDFParseException("Unknown XObject subtype: " + type); + } + result.add(PDFPage.createPopCmd()); + result.add(PDFPage.createPopCmd()); + return result; + } + + /** + * Transform to the position of the stamp annotation + * @return + */ + private AffineTransform getPositionTransformation() { + Float rect2 = getRect(); + double[] f = new double[] {1, + 0, + 0, + 1, + rect2.getMinX(), + rect2.getMinY()}; + return new AffineTransform(f); + } + + /** + * @return the onAppearance + */ + public PDFObject getOnAppearance() { + return onAppearance; + } + + /** + * @return the offAppearance + */ + public PDFObject getOffAppearance() { + return offAppearance; + } + + /** + * @return the appearanceStateOn + */ + public boolean isAppearanceStateOn() { + return appearanceStateOn; + } + + public void switchAppearance() { + this.appearanceStateOn = !this.appearanceStateOn; + } + + public PDFObject getCurrentAppearance() { + return appearanceStateOn?onAppearance:offAppearance; + } + + public List getCurrentCommand() { + return appearanceStateOn?onCmd:offCmd; + } + + @Override + public List getPageCommandsForAnnotation() { + List pageCommandsForAnnotation = super.getPageCommandsForAnnotation(); + pageCommandsForAnnotation.addAll(getCurrentCommand()); + return pageCommandsForAnnotation; + } + + /** + * @return the popupAnnotation + */ + public PDFAnnotation getPopupAnnotation() { + return popupAnnotation; + } + + public String getTextLabel() { + return textLabel; + } + +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/PDFAnnotation.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/PDFAnnotation.java new file mode 100644 index 0000000000..fca3ea9979 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/PDFAnnotation.java @@ -0,0 +1,235 @@ +package com.github.librepdf.pdfrenderer.annotation; + +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.awt.geom.Rectangle2D.Float; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; + +import com.github.librepdf.pdfrenderer.PDFCmd; +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/***************************************************************************** + * Encapsulate a PDF annotation. This is only the super-class of PDF annotations, + * which has an "unknown" annotation type. + * Use the createAnnotation() method for getting an annotation of the correct + * type (if implemented). + * + * @author Katja Sondermann + * @since 03.07.2009 + ****************************************************************************/ +public class PDFAnnotation{ + + /** Definition of some annotation sub-types*/ + public static final String GOTO = "GoTo"; + public static final String GOTOE = "GoToE"; + public static final String GOTOR = "GoToR"; + public static final String URI = "URI"; + + public enum Flags { + UNKNOWN, // 0 + INVISIBLE, // 1 + HIDDEN, // 2 + PRINT, // 3 + NO_ZOOM, // 4 + NO_ROTATE, // 5 + NO_VIEW, // 6 + READ_ONLY, // 7 + LOCKED, // 8 + TOGGLE_NO_VIEW, // 9 + LOCKED_CONTENTS // 10 + } + + + private final PDFObject pdfObj; + private final AnnotationType type; + private final Float rect; + private final String subType; + private final String contents; + + private final String annotationName; + private String modified; + private Integer flags; + private String appearanceState; + + /************************************************************************* + * Constructor + * @param annotObject - the PDFObject which contains the annotation description + * @throws IOException + ************************************************************************/ + public PDFAnnotation(PDFObject annotObject) throws IOException{ + this(annotObject, AnnotationType.UNKNOWN); + } + + /************************************************************************* + * Constructor + * @param annotObject - the PDFObject which contains the annotation description + * @throws IOException + ************************************************************************/ + public PDFAnnotation(PDFObject annotObject, AnnotationType type) throws IOException{ + this.pdfObj = annotObject; + // in case a general "PdfAnnotation" is created the type is unknown + this.type = type; + + this.subType = annotObject.getDictRefAsString("Subtype"); + this.contents = annotObject.getDictRefAsString("Contents"); + this.annotationName = annotObject.getDictRefAsString("NM"); + this.modified = annotObject.getDictRefAsString("M"); + this.flags = annotObject.getDictRefAsInt("F"); + this.appearanceState = annotObject.getDictRefAsString("AS"); + + // TODO add Border, C, StructParent, OC + + this.rect = this.parseRect(annotObject.getDictRef("Rect")); + } + + /************************************************************************* + * Create a new PDF annotation object. + * + * Currently supported annotation types: + *
  • Link annotation
  • + * + * @param parent + * @return PDFAnnotation + * @throws IOException + ************************************************************************/ + public static PDFAnnotation createAnnotation(PDFObject parent) throws IOException{ + PDFObject subtypeValue = parent.getDictRef("Subtype"); + if(subtypeValue == null) { + return null; + } + String subtypeS = subtypeValue.getStringValue(); + AnnotationType annotationType = AnnotationType.getByDefinition(subtypeS); + + //if Subtype is Widget than check if it is also a Signature + if(annotationType == AnnotationType.WIDGET) { + PDFObject sigType = parent.getDictRef("FT"); + if(sigType != null) { + String sigTypeS = sigType.getStringValue(); + if(AnnotationType.getByDefinition(sigTypeS) == AnnotationType.SIGNATURE) { + annotationType = AnnotationType.getByDefinition(sigTypeS); + } + } + } + + if(annotationType.displayAnnotation()) { + Class className = annotationType.getClassName(); + + try { + if (className.equals(MarkupAnnotation.class) || className.equals(TextMarkupAnnotation.class)) { + Constructor constructor = className.getConstructor(PDFObject.class, AnnotationType.class); + return (PDFAnnotation)constructor.newInstance(parent, annotationType); + } else { + Constructor constructor = className.getConstructor(PDFObject.class); + return (PDFAnnotation)constructor.newInstance(parent); + } + } catch (Exception e) { + throw new PDFParseException("Could not parse annotation!", e); + } + } + + return null; + } + + /** + * Get a Rectangle2D.Float representation for a PDFObject that is an + * array of four Numbers. + * @param obj a PDFObject that represents an Array of exactly four + * Numbers. + */ + public Rectangle2D.Float parseRect(PDFObject obj) throws IOException { + if (obj.getType() == PDFObject.ARRAY) { + PDFObject bounds[] = obj.getArray(); + if (bounds.length == 4) { + return new Rectangle2D.Float(bounds[0].getFloatValue(), + bounds[1].getFloatValue(), + bounds[2].getFloatValue() - bounds[0].getFloatValue(), + bounds[3].getFloatValue() - bounds[1].getFloatValue()); + } else { + throw new PDFParseException("Rectangle definition didn't have 4 elements"); + } + } else { + throw new PDFParseException("Rectangle definition not an array"); + } + } + + /************************************************************************* + * Get the PDF Object which contains the annotation values + * @return PDFObject + ************************************************************************/ + public PDFObject getPdfObj() { + return this.pdfObj; + } + + /************************************************************************* + * Get the annotation type + * @return int + ************************************************************************/ + public AnnotationType getType() { + return this.type; + } + + /************************************************************************* + * Get the rectangle on which the annotation should be applied to + * @return Rectangle2D.Float + ************************************************************************/ + public Float getRect() { + return this.rect; + } + + public String getSubType() { + return subType; + } + + public String getAnnotationName() { + return annotationName; + } + + public String getAppearanceState() { + return appearanceState; + } + + public String getContents() { + return contents; + } + + public Integer getFlags() { + return flags; + } + + public boolean isFlagSet(Flags flag) { + return flags != null + && BigInteger.valueOf(flags).testBit(flag.ordinal()); + } + + public String getModified() { + return modified; + } + + @Override + public String toString() { + return this.pdfObj.toString(); + } + + /** + * Get list of pdf commands for this annotation + * @return + */ + public List getPageCommandsForAnnotation() { + return new ArrayList(); + } + + + protected AffineTransform getScalingTransformation(Float bbox) { + AffineTransform at = new AffineTransform(); + double scaleHeight = getRect().getHeight()/bbox.getHeight(); + double scaleWidth = getRect().getWidth()/bbox.getWidth(); + at.scale(scaleWidth, scaleHeight); + return at; + } + +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/SquareAnnotation.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/SquareAnnotation.java new file mode 100644 index 0000000000..15f1c077d4 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/SquareAnnotation.java @@ -0,0 +1,25 @@ +package com.github.librepdf.pdfrenderer.annotation; + +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.PDFObject; + +/***************************************************************************** + * PDF annotation for a square + * + * @author Bernd Rosstauscher + ****************************************************************************/ +public class SquareAnnotation extends MarkupAnnotation { + + // TODO Not all of this is fully implemented yet. + // But it will work if the visual representation is done via an "Appearance Stream" + + /************************************************************************* + * Constructor + * @param annotObject + * @throws IOException + ************************************************************************/ + public SquareAnnotation(PDFObject annotObject) throws IOException { + super(annotObject, AnnotationType.SQUARE); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/StampAnnotation.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/StampAnnotation.java new file mode 100644 index 0000000000..b9d2f65a59 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/StampAnnotation.java @@ -0,0 +1,70 @@ +package com.github.librepdf.pdfrenderer.annotation; + +import java.io.IOException; +import java.util.List; + +import com.github.librepdf.pdfrenderer.PDFCmd; +import com.github.librepdf.pdfrenderer.PDFObject; + +/***************************************************************************** + * PDF annotation describing a stamp + * + * @author Katja Sondermann + * @since 26.03.2012 + ****************************************************************************/ +public class StampAnnotation extends MarkupAnnotation { + + private String iconName; + private List iconCommands; + + /************************************************************************* + * Constructor + * @param annotObject + * @throws IOException + ************************************************************************/ + public StampAnnotation(PDFObject annotObject) throws IOException { + super(annotObject, AnnotationType.STAMP); + this.iconName = annotObject.getDictRefAsString("Name"); + + // No AP so use the icon name + if (iconName != null && annotObject.getDictRef("AP") == null) { + parseIconCommands(); + } + + } + + /** + * If the stamp is represented by one of the predefined icons + * this will parse it and create PDFCommands for them. + */ + private void parseIconCommands() { + // TODO Add code for the different icon constants. + // fill iconCommands + + // These command names exist. + + // Approved, Experimental, NotApproved, AsIs, Expired , + // NotForPublicRelease, Confidential, Final, Sold, + // Departmental, ForComment, TopSecret, Draft, ForPublicRelease + } + + /** + * @return the iconName + */ + public String getIconName() { + return iconName; + } + + /** + *@return the PDF commands to render this annotation + */ + @Override + public List getCurrentCommand() { + List apCommand = super.getCurrentCommand(); + if (apCommand != null) { + return apCommand; + } + return this.iconCommands; + } + +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/TextMarkupAnnotation.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/TextMarkupAnnotation.java new file mode 100644 index 0000000000..a8923c83dc --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/TextMarkupAnnotation.java @@ -0,0 +1,75 @@ +package com.github.librepdf.pdfrenderer.annotation; + +import java.awt.Rectangle; +import java.awt.geom.GeneralPath; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.github.librepdf.pdfrenderer.PDFCmd; +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFPage; +import com.github.librepdf.pdfrenderer.PDFShapeCmd; + +/***************************************************************************** + * PDF annotation describing a text markup: Highlight, Underline, Squiggle, StrikeOut + * + * @author Bernd Rosstauscher + ****************************************************************************/ +public class TextMarkupAnnotation extends MarkupAnnotation { + + private int[] quadPoints; + private List highlightCommands; + + /************************************************************************* + * Constructor + * @param annotObject + * @throws IOException + ************************************************************************/ + public TextMarkupAnnotation(PDFObject annotObject, AnnotationType annotationType) throws IOException { + super(annotObject, annotationType); + this.quadPoints = annotObject.getDictRefAsIntArray("QuadPoints"); + + // No AP so use the quad points and highlight mode + if (annotObject.getDictRef("AP") == null) { + parseHighlightCommands(); + } + + } + + /** + * Parse the highlight commands + */ + private void parseHighlightCommands() { + // invalid quad points + if (this.quadPoints == null || this.quadPoints.length % 4 != 0) { + return; + } + highlightCommands = new ArrayList(); + highlightCommands.add(PDFPage.createPushCmd()); + + //TODO currently we use the same code for: Highlight, Underline, Squiggle, StrikeOut + // We should also set the correct colors and such. + + // Draw a box + for (int i = 0; i < quadPoints.length; i+=4) { + GeneralPath gp = new GeneralPath(new Rectangle(quadPoints[i], quadPoints[i+1], quadPoints[i+2], quadPoints[i+3])); + highlightCommands.add(new PDFShapeCmd(gp, PDFShapeCmd.FILL, true)); + } + highlightCommands.add(PDFPage.createPopCmd()); + } + + /** + * Gets the highlight painting commands + * Use either Quads or Appearance Stream + */ + @Override + public List getCurrentCommand() { + List apCommand = super.getCurrentCommand(); + if (apCommand != null) { + return apCommand; + } + return this.highlightCommands; + } + +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/WidgetAnnotation.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/WidgetAnnotation.java new file mode 100644 index 0000000000..b63e8910fc --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/annotation/WidgetAnnotation.java @@ -0,0 +1,300 @@ +package com.github.librepdf.pdfrenderer.annotation; + +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.awt.geom.Rectangle2D.Float; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import com.github.librepdf.pdfrenderer.PDFCmd; +import com.github.librepdf.pdfrenderer.PDFImage; +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFPage; +import com.github.librepdf.pdfrenderer.PDFParseException; +import com.github.librepdf.pdfrenderer.PDFParser; + +/** + * PDF annotation describing a widget. + * @since Aug 20, 2010 + */ +public class WidgetAnnotation extends PDFAnnotation { + + private String fieldValue; + private FieldType fieldType; + private String fieldName; + private PDFObject fieldValueRef; + private List cmd; + + /** + * Type for PDF form elements + * @version $Id: WidgetAnnotation.java,v 1.2 2010-09-30 10:34:44 xphc Exp $ + * @author xphc + * @since Aug 20, 2010 + */ + public enum FieldType { + /** Button Field */ + Button("Btn"), + /** Text Field */ + Text("Tx"), + /** Choice Field */ + Choice("Ch"), + /** Signature Field */ + Signature("Sig"); + + private final String typeCode; + + FieldType(String typeCode) { + this.typeCode = typeCode; + } + + static FieldType getByCode(String typeCode) { + FieldType[] values = values(); + for (FieldType value : values) { + if (value.typeCode.equals(typeCode)) + return value; + } + return null; + } + } + + public WidgetAnnotation(PDFObject annotObject) throws IOException { + super(annotObject, AnnotationType.WIDGET); + + // The type of field that this dictionary describes. Field type is + // present for terminal fields but is inherited from parent if absent + // (see PDF Reference 1.7 table 8.69) + PDFObject fieldTypeRef = annotObject.getDictRef("FT"); + if (fieldTypeRef != null) { + // terminal field + this.fieldType = FieldType.getByCode(fieldTypeRef.getStringValue()); + } + else { + // must check parent since field type is inherited + PDFObject parent = annotObject.getDictRef("Parent"); + while (parent != null && parent.isIndirect()) { + parent = parent.dereference(); + } + if (parent != null) { + fieldTypeRef = parent.getDictRef("FT"); + this.fieldType = FieldType.getByCode(fieldTypeRef.getStringValue()); + } + } + + // Name defined for the field + PDFObject fieldNameRef = annotObject.getDictRef("T"); + if (fieldNameRef != null) { + this.fieldName = fieldNameRef.getTextStringValue(); + } + this.fieldValueRef = annotObject.getDictRef("V"); + if (this.fieldValueRef != null) { + this.fieldValue = this.fieldValueRef.getTextStringValue(); + } + parseAP(annotObject.getDictRef("AP")); + } + + private void parseAP(PDFObject dictRef) throws IOException { + if(dictRef == null) { + return; + } + PDFObject normalAP = dictRef.getDictRef("N"); + if(normalAP == null) { + return; + } + cmd = parseCommand(normalAP); + } + + /** + * @param obj + * @return + * @throws IOException + */ + private List parseCommand(PDFObject obj) throws IOException { + // TODO see also MarkupAnnotation.parseIntoPdfCommands() which seems to be copied code + // We should merge these two + PDFObject dictRefSubType = obj.getDictRef("Subtype"); + String type = null; + if(dictRefSubType != null) { + type = dictRefSubType.getStringValue(); + } + + if (type == null) { + PDFObject dictRefS = obj.getDictRef("S"); + if(dictRefS != null) { + type = dictRefS.getStringValue(); + } + } + + //if type is still null, check for AcroForm, if AcroForm is available the PDF could be not compatible + //with the PDF specification, anyway check if obj is in AcroForm, if so, proceed as for a good PDF + if(type == null) { + PDFObject acroForm = obj.getRoot().getDictRef("AcroForm"); + PDFObject fields = acroForm.getDictRef("Fields"); + PDFObject[] arrayFields = fields.getArray(); + + for (PDFObject pdfObject : arrayFields) { + PDFObject dictRefAP = pdfObject.getDictRef("AP"); + if(dictRefAP != null) { + PDFObject dictRefN = dictRefAP.getDictRef("N"); + + if(dictRefN.equals(obj)) { + PDFObject dictRefAS = pdfObject.getDictRef("AS"); + if(dictRefAS != null) { //this is a combobox + PDFObject dictRef = dictRefN.getDictRef(dictRefAS.getStringValue()); + obj = dictRef; + } + + type = "Form"; + break; + } + } + } + + if(type == null) { //check for radiobutton + PDFObject dictRef = obj.getDictRef("Off"); + if(dictRef != null) { + for (PDFObject pdfObject : arrayFields) { + PDFObject dictRefT = pdfObject.getDictRef("T"); + if(dictRefT != null && dictRefT.getStringValue().contains("Group")) { + PDFObject kids = pdfObject.getDictRef("Kids"); + PDFObject[] arrayKids = kids.getArray(); + for (PDFObject kid : arrayKids) { + PDFObject kidAP = kid.getDictRef("AP"); + PDFObject kidN = kidAP.getDictRef("N"); + if(kidN.equals(obj)) { + PDFObject kidAS = kid.getDictRef("AS"); + if(kidAS != null) { + PDFObject kidRef = kidN.getDictRef(kidAS.getStringValue()); + obj = kidRef; + } + + type = "Form"; + break; + } + } + } + } + } + } + } + + ArrayList result = new ArrayList(); + result.add(PDFPage.createPushCmd()); + result.add(PDFPage.createPushCmd()); + if ("Image".equals(type)) { + // stamp annotation transformation + AffineTransform rectAt = getPositionTransformation(); + result.add(PDFPage.createXFormCmd(rectAt)); + + PDFImage img = PDFImage.createImage(obj, new HashMap() , false); + result.add(PDFPage.createImageCmd(img)); + } else if ("Form".equals(type)) { + // rats. parse it. + PDFObject bobj = obj.getDictRef("BBox"); + Float bbox = new Rectangle2D.Float(bobj.getAt(0).getFloatValue(), + bobj.getAt(1).getFloatValue(), + bobj.getAt(2).getFloatValue(), + bobj.getAt(3).getFloatValue()); + PDFPage formCmds = new PDFPage(bbox, 0); + // stamp annotation transformation + AffineTransform rectAt = getPositionTransformation(); + formCmds.addXform(rectAt); + + AffineTransform rectScaled = getScalingTransformation(bbox); + formCmds.addXform(rectScaled); + + // form transformation + AffineTransform at; + PDFObject matrix = obj.getDictRef("Matrix"); + if (matrix == null) { + at = new AffineTransform(); + } else { + float elts[] = new float[6]; + for (int i = 0; i < elts.length; i++) { + elts[i] = (matrix.getAt(i)).getFloatValue(); + } + at = new AffineTransform(elts); + } + formCmds.addXform(at); + + HashMap r = new HashMap(new HashMap()); + PDFObject rsrc = obj.getDictRef("Resources"); + if (rsrc != null) { + r.putAll(rsrc.getDictionary()); + } + + PDFParser form = new PDFParser(formCmds, obj.getStream(), r); + form.go(true); + + result.addAll(formCmds.getCommands()); + } else { + throw new PDFParseException("Unknown XObject subtype: " + type); + } + result.add(PDFPage.createPopCmd()); + result.add(PDFPage.createPopCmd()); + return result; + } + + /** + * Transform to the position of the stamp annotation + * @return + */ + private AffineTransform getPositionTransformation() { + Float rect2 = getRect(); + double[] f = new double[] {1, + 0, + 0, + 1, + rect2.getMinX(), + rect2.getMinY()}; + return new AffineTransform(f); + } + + /** + * Returns the type of the field + * @return Field type + */ + public FieldType getFieldType() { + return this.fieldType; + } + + /** + * The field's value as a string. Might be {@code null}. + * @return The field value or {@code null}. + */ + public String getFieldValue() { + return this.fieldValue; + } + + /** + * Sets the field value for a text field. Note: this doesn't actually change + * the PDF file yet. + * + * @param fieldValue + * The new value for the text field + */ + public void setFieldValue(String fieldValue) { + this.fieldValue = fieldValue; + } + + /** + * Name for this widget. + * @return Widget name + */ + public String getFieldName() { + return this.fieldName; + } + + @Override + public List getPageCommandsForAnnotation() { + List pageCommandsForAnnotation = super.getPageCommandsForAnnotation(); + // cmd might be null if there is no AP (appearance dictionary) + // AP is optional see PDF Reference 1.7 table 8.15 + if (this.cmd != null) { + pageCommandsForAnnotation.addAll(this.cmd); + } + return pageCommandsForAnnotation; + } + +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/AltColorSpace.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/AltColorSpace.java new file mode 100644 index 0000000000..0c795f14fd --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/AltColorSpace.java @@ -0,0 +1,73 @@ +package com.github.librepdf.pdfrenderer.colorspace; + +import java.awt.color.ColorSpace; + +import com.github.librepdf.pdfrenderer.function.PDFFunction; + +/***************************************************************************** + * Color Space implementation for handling the PDF AlternateColorSpace. + * A PDF function is applied to colorvalues before converting. + * + * @author Katja Sondermann + * @since 06.01.2011 + ****************************************************************************/ +public class AltColorSpace extends ColorSpace { + + private PDFFunction fkt; + private ColorSpace origCs; + /** + * Create a new CMYKColorSpace Instance. + */ + public AltColorSpace(PDFFunction fkt, ColorSpace origCs) { + super(origCs.getType(), fkt.getNumInputs()); + this.fkt = fkt; + this.origCs = origCs; + } + + /** + * Converts from CIEXYZ. + * + * @see java.awt.color.ColorSpace#fromCIEXYZ(float[]) + * @see org.scantegrity.lib.CMYKColorSpace#toCIEXYZ + */ + @Override + public float[] fromCIEXYZ(float[] p_colorvalue) { + p_colorvalue = this.fkt.calculate(p_colorvalue); + return this.origCs.fromCIEXYZ(p_colorvalue); + } + + /** + * Converts a given RGB. + * + * @param p_rgbvalue - The color to translate + * @return a float[4] of the CMYK values. + * @see java.awt.color.ColorSpace#fromRGB(float[]) + */ + @Override + public float[] fromRGB(float[] p_rgbvalue) { + p_rgbvalue = this.fkt.calculate(p_rgbvalue); + return this.origCs.fromCIEXYZ(p_rgbvalue); + } + + /** + * Converts to CIEXYZ. + * @see java.awt.color.ColorSpace#toCIEXYZ(float[]) + */ + @Override + public float[] toCIEXYZ(float[] p_colorvalue) { + float[] colorvalue = this.fkt.calculate(p_colorvalue); + return this.origCs.toCIEXYZ(colorvalue); + } + + /** + * Converts to RGB. + * + * @param p_colorvalue The color in CMYK. + * @see java.awt.color.ColorSpace#toRGB(float[]) + */ + @Override + public float[] toRGB(float[] p_colorvalue) { + float[] colorvalue = this.fkt.calculate(p_colorvalue); + return this.origCs.toRGB(colorvalue); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/AlternateColorSpace.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/AlternateColorSpace.java new file mode 100644 index 0000000000..9df5dc9376 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/AlternateColorSpace.java @@ -0,0 +1,95 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.colorspace; + +import java.awt.color.ColorSpace; + +import com.github.librepdf.pdfrenderer.PDFPaint; +import com.github.librepdf.pdfrenderer.function.PDFFunction; + +/** + * A color space that uses another color space to return values, and a + * function to map between values in the input and input values to the + * alternate color space + */ +public class AlternateColorSpace extends PDFColorSpace { + /** The alternate color space */ + private PDFColorSpace alternate; + + /** The function */ + private PDFFunction function; + + private AltColorSpace altcolorspace; + + /** Creates a new instance of AlternateColorSpace */ + public AlternateColorSpace(PDFColorSpace alternate, PDFFunction function) { + super(null); + + this.alternate = alternate; + this.function = function; + } + + /** + * get the number of components expected in the getPaint command + */ + @Override public int getNumComponents() { + if (this.function != null) { + return this.function.getNumInputs(); + } else { + return this.alternate.getNumComponents(); + } + } + + /** + * get the PDFPaint representing the color described by the + * given color components + * @param components the color components corresponding to the given + * colorspace + * @return a PDFPaint object representing the closest Color to the + * given components. + */ + @Override public PDFPaint getPaint(float[] components) { + if (this.function != null) { + // translate values using function + components = this.function.calculate(components); + } + + return this.alternate.getPaint(components); + } + + /** + * get the original Java ColorSpace. + */ + @Override public ColorSpace getColorSpace() { + if (altcolorspace == null) altcolorspace = new AltColorSpace(function, alternate.getColorSpace()); + return altcolorspace; + //return this.alternate.getColorSpace(); + } + + /************************************************************************* + * Get the PDF function + * @return PDFFunction + ************************************************************************/ + public PDFFunction getFunktion() { + return this.function; + } + + +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/CMYKColorSpace.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/CMYKColorSpace.java new file mode 100644 index 0000000000..1a297dfc67 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/CMYKColorSpace.java @@ -0,0 +1,79 @@ +package com.github.librepdf.pdfrenderer.colorspace; + +import java.awt.color.ColorSpace; + +public class CMYKColorSpace extends ColorSpace { + + private static final long serialVersionUID = 1L; + + /** + * Create a new CMYKColorSpace instance. + */ + public CMYKColorSpace() { + super(ColorSpace.TYPE_CMYK, 4); + } + + @Override + public float[] fromCIEXYZ(float[] colorvalue) { + // Convert CIEXYZ to RGB first + ColorSpace sRGB = ColorSpace.getInstance(ColorSpace.CS_sRGB); + float[] rgb = sRGB.fromCIEXYZ(colorvalue); + // Convert RGB to CMYK + return fromRGB(rgb); + } + + @Override + public float[] fromRGB(float[] rgbvalue) { + // Convert RGB to CMY + float c = 1 - rgbvalue[0]; + float m = 1 - rgbvalue[1]; + float y = 1 - rgbvalue[2]; + + // Extract the black key (K) + float k = Math.min(c, Math.min(m, y)); + + // Avoid division by zero and ensure we don't get negative values + float divisor = (1 - k) == 0 ? 1 : (1 - k); + float cC = (c - k) / divisor; + float mC = (m - k) / divisor; + float yC = (y - k) / divisor; + + return new float[]{cC, mC, yC, k}; + } + + @Override + public float[] toCIEXYZ(float[] colorvalue) { + // Convert CMYK to RGB first + float[] rgb = toRGB(colorvalue); + // Convert RGB to CIEXYZ + ColorSpace sRGB = ColorSpace.getInstance(ColorSpace.CS_sRGB); + return sRGB.toCIEXYZ(rgb); + } + + @Override + public float[] toRGB(float[] cmykvalue) { + float c = cmykvalue[0]; + float m = cmykvalue[1]; + float y = cmykvalue[2]; + float k = cmykvalue[3]; + + float r = (1 - c) * (1 - k); + float g = (1 - m) * (1 - k); + float b = (1 - y) * (1 - k); + + return new float[]{r, g, b}; + } + + /** + * Normalize ensures all color values returned are between 0 and 1. + * + * @param colors the color values to normalize + * @return the normalized color values + */ + private float[] normalize(float[] colors) { + for (int i = 0; i < colors.length; i++) { + colors[i] = Math.min(1.0f, Math.max(0.0f, colors[i])); + } + return colors; + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/CalGrayColor.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/CalGrayColor.java new file mode 100644 index 0000000000..f7e1365a1d --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/CalGrayColor.java @@ -0,0 +1,133 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.colorspace; + +import java.awt.color.ColorSpace; +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.PDFObject; + +/** + * A ColorSpace for calibrated gray + * @author Mike Wessler + */ +public class CalGrayColor extends ColorSpace { + float white[]= {1f, 1f, 1f}; + float black[]= {0, 0, 0}; + float gamma= 1; + static ColorSpace cie= ColorSpace.getInstance(ColorSpace.CS_sRGB); + + /** + * Create a new Calibrated Gray color space object, given + * the description in a PDF dictionary. + * @param obj a dictionary that contains an Array of 3 Numbers + * for "WhitePoint" and "BlackPoint", and a Number for "Gamma" + */ + public CalGrayColor(PDFObject obj) throws IOException { + // obj is a dictionary that has the following parts: + // WhitePoint [a b c] + // BlackPoint [a b c] + // Gamma a + super(TYPE_GRAY, 1); + PDFObject ary= obj.getDictRef("WhitePoint"); + if (ary!=null) { + for(int i=0; i<3; i++) { + this.white[i]= ary.getAt(i).getFloatValue(); + } + } + ary= obj.getDictRef("BlackPoint"); + if (ary!=null) { + for(int i=0; i<3; i++) { + this.black[i]= ary.getAt(i).getFloatValue(); + } + } + PDFObject g= obj.getDictRef("Gamma"); + if (g!=null) { + this.gamma= g.getFloatValue(); + } + } + + /** + * Create a new calibrated gray color space object, with the + * default values for black point, white point and gamma + */ + public CalGrayColor() { + super(TYPE_GRAY, 1); + } + + /** + * get the number of components (1). + */ + @Override public int getNumComponents() { + return 1; + } + + /** + * convert from Calibrated Gray to RGB. + * @param comp the gray value (0-1) + * @return the RGB values (0-1) + */ + @Override + public float[] toRGB(float comp[]) { + if (comp.length==1) { + float mul= (float)Math.pow(comp[0], this.gamma); + float xyz[] = { + this.white[0]*mul, + 0, + 0}; + float rgb[]= cie.fromCIEXYZ(xyz); + return rgb; + } else { + return this.black; + } + } + + /** + * convert from RGB to Calibrated Gray. NOT IMPLEMENTED + */ + @Override + public float[] fromRGB(float[] rgbvalue) { + return new float[1]; + } + + /** + * convert from CIEXYZ to Calibrated Gray. NOT IMPLEMENTED + */ + @Override + public float[] fromCIEXYZ(float[] colorvalue) { + return new float[1]; + } + + /** + * get the type of this ColorSpace (TYPE_GRAY) + */ + @Override public int getType() { + return TYPE_GRAY; + } + + /** + * convert from Calibrated Gray to CIEXYZ. NOT IMPLEMENTED + */ + @Override + public float[] toCIEXYZ(float[] colorvalue) { + return new float[3]; + } + +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/CalRGBColor.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/CalRGBColor.java new file mode 100644 index 0000000000..b75dc705fd --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/CalRGBColor.java @@ -0,0 +1,250 @@ +/* + * $Id: CalRGBColor.java,v 1.1 2009-07-01 12:43:19 bros Exp $ + * + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.colorspace; + +import java.awt.color.ColorSpace; +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.function.FunctionType0; + +/** + * A ColorSpace for calibrated RGB + * @author Mike Wessler + */ +public class CalRGBColor extends ColorSpace { + private static final float[] vonKriesM = { 0.40024f, -0.22630f, 0.00000f, + 0.70760f, 1.16532f, 0.00000f, + -0.08081f, 0.04570f, 0.91822f }; + private static final float[] vonKriesMinv = { 1.859936f, 0.361191f, 0.000000f, + -1.129382f, 0.638812f, 0.000000f, + 0.219897f, -0.000006f, 1.089064f }; + private static final float[] xyzToSRGB = { 3.24071f, -0.969258f, 0.0556352f, + -1.53726f, 1.87599f, -0.203996f, + -0.498571f, 0.0415557f, 1.05707f }; + + private static final float[] xyzToRGB = { 2.04148f, -0.969258f, 0.0134455f, + -0.564977f, 1.87599f, -0.118373f, + -0.344713f, 0.0415557f, 1.01527f }; + + float[] scale; + float[] max; + + float white[]= {1f, 1f, 1f}; + float black[]= {0, 0, 0}; + float matrix[]= {1f, 0, 0, 0, 1f, 0, 0, 0, 1f}; + float gamma[]= {1f, 1f, 1f}; + + static ColorSpace rgbCS= ColorSpace.getInstance(ColorSpace.CS_sRGB); + static ColorSpace cieCS= ColorSpace.getInstance(ColorSpace.CS_CIEXYZ); + + /** + * Create a new Calibrated RGB color space object, given the + * description in a PDF dictionary. + * @param obj a dictionary that contains an array of 3 Numbers + * for "WhitePoint" and "BlackPoint", a Number for "Gamma", and + * an array of 9 Numbers for "Matrix". + */ + public CalRGBColor(PDFObject obj) throws IOException { + // obj is a dictionary that has the following parts: + // WhitePoint [a b c] + // BlackPoint [a b c] + // Gamma a + super(CS_sRGB, 3); + + // find out what what is according to the CIE color space + // note that this is not reflexive (i.e. passing this value + // into toRGB does not get you (1.0, 1.0, 1.0) back) + // cieWhite = cieCS.fromRGB(new float[] { 1.0f, 1.0f, 1.0f } ); + + PDFObject ary= obj.getDictRef("WhitePoint"); + if (ary!=null) { + for(int i=0; i<3; i++) { + this.white[i]= ary.getAt(i).getFloatValue(); + } + } + ary= obj.getDictRef("BlackPoint"); + if (ary!=null) { + for(int i=0; i<3; i++) { + this.black[i]= ary.getAt(i).getFloatValue(); + } + } + ary= obj.getDictRef("Gamma"); + if (ary!=null) { + for (int i=0; i<3; i++) { + this.gamma[i]= ary.getAt(i).getFloatValue(); + } + } + ary= obj.getDictRef("Matrix"); + if (ary!=null) { + for (int i=0; i<9; i++) { + this.matrix[i]= ary.getAt(i).getFloatValue(); + } + } + + // create a scale matrix relative to the 50 CIE color space. + // see http://www.brucelindbloom.com/Eqn_RGB_XYZ_Matrix.html + // we use the Von Kries cone response domain + float[] cieWhite = rgbCS.toCIEXYZ(new float[] { 1f, 1f, 1f }); + + float[] sourceWhite = matrixMult(this.white, vonKriesM, 3); + float[] destWhite = matrixMult(cieWhite, vonKriesM, 3); + + this.scale = new float[] { destWhite[0] / sourceWhite[0], 0, 0, + 0, destWhite[1] / sourceWhite[1], 0, + 0, 0, destWhite[2] / sourceWhite[2] }; + this.scale = matrixMult(vonKriesM, this.scale, 3); + this.scale = matrixMult(this.scale, vonKriesMinv, 3); + + this.max = matrixMult(this.white, this.scale, 3); + this.max = ciexyzToSRGB(this.max); + } + + /** + * get the number of components (3) + */ + @Override public int getNumComponents() { + return 3; + } + + /** + * convert from Calibrated RGB to standard RGB + * @param comp the Calibrated RGB values (0-1) + * @return the RGB values (0-1) + */ + @Override + public float[] toRGB(float comp[]) { + if (comp.length==3) { + // compute r', g' and b' by raising the given values to the + // correct gamma + float a = (float)Math.pow(comp[0], this.gamma[0]); + float b = (float)Math.pow(comp[1], this.gamma[1]); + float c = (float)Math.pow(comp[2], this.gamma[2]); + + // now multiply by the matrix to get X, Y and Z values + float[] xyz = new float[] { + this.matrix[0]*a + this.matrix[3]*b + this.matrix[6]*c, + this.matrix[1]*a + this.matrix[4]*b + this.matrix[7]*c, + this.matrix[2]*a + this.matrix[5]*b + this.matrix[8]*c}; + + // now scale the xyz values + xyz = matrixMult(xyz, this.scale, 3); + + // convert to RGB + float[] rgb = ciexyzToSRGB(xyz); + + // cheat -- scale based on max + for (int i = 0; i < rgb.length; i++) { + rgb[i] = FunctionType0.interpolate(rgb[i], 0, this.max[i], 0, 1); + + // sometimes we get off a little bit due to precision loss + if (rgb[i] > 1.0) { + rgb[i] = 1.0f; + } + } + + return rgb; + } else { + return this.black; + } + } + + /** + * Convert from CIEXYZ, with scale and gamma calculated to sRGB + */ + private float[] ciexyzToSRGB(float[] xyz) { + float[] rgb = matrixMult(xyz, xyzToSRGB, 3); + + for (int i = 0; i < rgb.length; i++) { + if (rgb[i] < 0.0) { + rgb[i] = 0f; + } else if (rgb[i] > 1.0) { + rgb[i] = 1f; + } + + if (rgb[i] < 0.003928) { + rgb[i] *= 12.92; + } else { + rgb[i] = (float) ((Math.pow(rgb[i], 1.0 / 2.4) * 1.055) - 0.055); + } + } + + //float[] rgb = cieCS.toRGB(xyz); + return rgb; + } + + /** + * convert from RGB to Calibrated RGB. NOT IMPLEMENTED + */ + @Override + public float[] fromRGB(float[] rgbvalue) { + return new float[3]; + } + + /** + * convert from CIEXYZ to Calibrated RGB. NOT IMPLEMENTED + */ + @Override + public float[] fromCIEXYZ(float[] colorvalue) { + return new float[3]; + } + + /** + * get the type of this color space (TYPE_RGB) + */ + @Override public int getType() { + return TYPE_RGB; + } + + /** + * convert from Calibrated RGB to CIEXYZ. NOT IMPLEMENTED + */ + @Override + public float[] toCIEXYZ(float[] colorvalue) { + return new float[3]; + } + + /** + * Slowly multiply two matrices + * + * @param a the first matrix + * @param b the second matrix + * @param len the size of each row. All matrix lengths must be a + * multiple of len. + */ + float[] matrixMult(float[] a, float[] b, int len) { + int rows = a.length / len; + int cols = b.length / len; + + float[] out = new float[rows * cols]; + + for (int i = 0; i < rows; i++) { + for (int k = 0; k < cols; k++) { + for (int j = 0; j < len; j++) { + out[(i * cols) + k] += a[(i * len) + j] * b[(j * cols) + k]; + } + } + } + + return out; + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/IndexedColor.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/IndexedColor.java new file mode 100644 index 0000000000..efa9bfc301 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/IndexedColor.java @@ -0,0 +1,137 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.colorspace; + +import java.awt.Color; +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFPaint; + +/** + * A PDFColorSpace for an IndexedColor model + * + * @author Mike Wessler + */ +public class IndexedColor extends PDFColorSpace { + + /** + * r,g,and b components of the color table as a single array, for + * Java's IndexColorModel */ + protected byte[] finalcolors; + /** the color table */ + Color table[]; + /** size of the color table */ + int count; + /** number of channels in the base Color Space (unused) */ + int nchannels = 1; + + /** + * create a new IndexColor PDFColorSpace based on another PDFColorSpace, + * a count of colors, and a stream of values. Every consecutive n bytes + * of the stream is interpreted as a color in the base ColorSpace, where + * n is the number of components in that color space. + * + * @param base the color space in which the data is interpreted + * @param count the number of colors in the table + * @param stream a stream of bytes. The number of bytes must be count*n, + * where n is the number of components in the base colorspace. + */ + public IndexedColor(PDFColorSpace base, int count, PDFObject stream) throws IOException { + super(null); + count++; + this.count = count; + byte[] data = stream.getStream(); + this.nchannels = base.getNumComponents(); + boolean offSized = (data.length / this.nchannels) < count; + this.finalcolors = new byte[3 * count]; + this.table = new Color[count]; + float comps[] = new float[this.nchannels]; + int loc = 0; + int finalloc = 0; + for (int i = 0; i < count; i++) { + for (int j = 0; j < comps.length; j++) { + if (loc < data.length) { + comps[j] = ((data[loc++]) & 0xff) / 255f; + } else { + comps[j] = 1.0f; + } + } + this.table[i] = (Color) base.getPaint(comps).getPaint(); + this.finalcolors[finalloc++] = (byte) this.table[i].getRed(); + this.finalcolors[finalloc++] = (byte) this.table[i].getGreen(); + this.finalcolors[finalloc++] = (byte) this.table[i].getBlue(); + } + } + + /** + * create a new IndexColor PDFColorSpace based on a table of colors. + * + * @param table an array of colors + */ + public IndexedColor(Color[] table) throws IOException { + super(null); + + this.count = table.length; + this.table = table; + + this.finalcolors = new byte[3 * this.count]; + this.nchannels = 3; + + int loc = 0; + + for (int i = 0; i < this.count; i++) { + this.finalcolors[loc++] = (byte) table[i].getRed(); + this.finalcolors[loc++] = (byte) table[i].getGreen(); + this.finalcolors[loc++] = (byte) table[i].getBlue(); + } + } + + /** + * Get the number of indices + */ + public int getCount() { + return this.count; + } + + /** + * Get the table of color components + */ + public byte[] getColorComponents() { + return this.finalcolors; + } + + /** + * get the number of components of this colorspace (1) + */ + @Override + public int getNumComponents() { + return 1; + } + + /** + * get the color represented by the index. + * @param components an array of exactly one integer number whose + * value is between 0 and the size of the color table - 1. + */ + @Override + public PDFPaint getPaint(float components[]) { + return PDFPaint.getPaint(this.table[(int) components[0]]); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/LabColor.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/LabColor.java new file mode 100644 index 0000000000..6a9f62f4f9 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/LabColor.java @@ -0,0 +1,138 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.colorspace; + +import java.awt.color.ColorSpace; +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.PDFObject; + +/** + * A ColorSpace for Lab color + * @author Mike Wessler + */ +public class LabColor extends ColorSpace { + float white[]= {1f, 1f, 1f}; + float black[]= {0, 0, 0}; + float range[]= {-100f, 100f, -100f, 100f}; + static ColorSpace cie= ColorSpace.getInstance(ColorSpace.CS_sRGB); + + /** + * Create a new Lab color space object, given the description in + * a PDF dictionary. + * @param obj a dictionary that contains an Array of 3 Numbers for + * "WhitePoint" and "BlackPoint", and an array of 4 Numbers for + * "Range". + */ + public LabColor(PDFObject obj) throws IOException { + // obj is a dictionary that has the following parts: + // WhitePoint [a b c] + // BlackPoint [a b c] + // Gamma a + super(TYPE_Lab, 3); + PDFObject ary= obj.getDictRef("WhitePoint"); + if (ary!=null) { + for(int i=0; i<3; i++) { + this.white[i]= ary.getAt(i).getFloatValue(); + } + } + ary= obj.getDictRef("BlackPoint"); + if (ary!=null) { + for(int i=0; i<3; i++) { + this.black[i]= ary.getAt(i).getFloatValue(); + } + } + ary= obj.getDictRef("Range"); + if (ary!=null) { + for (int i=0; i<4; i++) { + this.range[i]= ary.getAt(i).getFloatValue(); + } + } + } + + /** + * get the number of components for this color space (3) + */ + @Override public int getNumComponents() { + return 3; + } + + /** + * Stage 2 of the conversion algorithm. Pulled out because + * it gets invoked for each component + */ + public final float stage2(float s1) { + return (s1>=6f/29f)?s1*s1*s1:108f/841f*(s1-4f/29f); + } + + /** + * convert from Lab to RGB + * @param comp the Lab values (0-1) + * @return the RGB values (0-1) + */ + @Override + public float[] toRGB(float comp[]) { + if (comp.length==3) { + float l= (comp[0]+16)/116+comp[1]/500; + float m= (comp[0]+16)/116; + float n= (comp[0]+16)/116-comp[2]/200; + float xyz[]= { + this.white[0]*stage2(l), + this.white[0]*stage2(m), + this.white[0]*stage2(n)}; + float rgb[]= cie.fromCIEXYZ(xyz); + return rgb; + } else { + return this.black; + } + } + + /** + * convert from RGB to Lab. NOT IMPLEMENTED + */ + @Override + public float[] fromRGB(float[] rgbvalue) { + return new float[3]; + } + + /** + * convert from CIEXYZ to Lab. NOT IMPLEMENTED + */ + @Override + public float[] fromCIEXYZ(float[] colorvalue) { + return new float[3]; + } + + /** + * get the type of this colorspace (TYPE_Lab) + */ + @Override public int getType() { + return TYPE_Lab; + } + + /** + * convert from Lab to CIEXYZ. NOT IMPLEMENTED + */ + @Override + public float[] toCIEXYZ(float[] colorvalue) { + return new float[3]; + } + +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/MaskColorSpace.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/MaskColorSpace.java new file mode 100644 index 0000000000..f8595fbb52 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/MaskColorSpace.java @@ -0,0 +1,101 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.colorspace; + +import java.awt.Color; +import java.awt.color.ColorSpace; + +import com.github.librepdf.pdfrenderer.PDFPaint; + +/** + * A color space used to implement masks. For now, the only type of mask + * supported is one where the image pixels specify where to paint, and the + * painting itself is done in a pre-specified PDF Paint. + */ +public class MaskColorSpace extends ColorSpace { + /** The paint to paint in. Note this cannot be a pattern or gradient. */ + private final PDFPaint paint; + + /** Creates a new instance of PaintColorSpace */ + public MaskColorSpace(PDFPaint paint) { + super (TYPE_RGB, 1); + + this.paint = paint; + } + + @Override + public float[] fromCIEXYZ(float[] colorvalue) { + float x = colorvalue[0]; + float y = colorvalue[1]; + float z = colorvalue[2]; + + float[] mask = new float[1]; + + if (Math.round(x) > 0 || Math.round(y) > 0 || Math.round(z) > 0) { + mask[0] = 1; + } else { + mask[0] = 0; + } + + return mask; + } + + @Override + public float[] fromRGB(float[] rgbvalue) { + float r = rgbvalue[0]; + float g = rgbvalue[1]; + float b = rgbvalue[2]; + + float[] mask = new float[1]; + + if (Math.round(r) > 0 || Math.round(g) > 0 || Math.round(b) > 0) { + mask[0] = 1; + } else { + mask[0] = 0; + } + + return mask; + } + + ColorSpace cie = ColorSpace.getInstance(ColorSpace.CS_CIEXYZ); + float[] prev1= this.cie.fromRGB(toRGB(new float[] {1.0f})); + float[] prev0= this.cie.fromRGB(toRGB(new float[] {0.0f})); + + @Override + public float[] toCIEXYZ(float[] colorvalue) { + if (colorvalue[0]==1) { + return this.prev1; + } else if (colorvalue[0]==0) { + return this.prev0; + } else { + return this.cie.fromRGB(toRGB(colorvalue)); + } + } + + @Override + public float[] toRGB(float[] colorvalue) { + return ((Color) this.paint.getPaint()).getRGBColorComponents(null); + } + + @Override public int getNumComponents() { + return 1; + } + +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/PDFColorSpace.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/PDFColorSpace.java new file mode 100644 index 0000000000..f685203083 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/PDFColorSpace.java @@ -0,0 +1,237 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.colorspace; + +import java.awt.Color; +import java.awt.color.ColorSpace; +import java.awt.color.ICC_ColorSpace; +import java.awt.color.ICC_Profile; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Map; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFPaint; +import com.github.librepdf.pdfrenderer.PDFParseException; +import com.github.librepdf.pdfrenderer.function.PDFFunction; + + +/** + * A color space that can convert a set of color components into + * PDFPaint. + * @author Mike Wessler + */ +public class PDFColorSpace { + /** the name of the device-dependent gray color space */ + public static final int COLORSPACE_GRAY = 0; + + /** the name of the device-dependent RGB color space */ + public static final int COLORSPACE_RGB = 1; + + /** the name of the device-dependent CMYK color space */ + public static final int COLORSPACE_CMYK = 2; + + /** the name of the pattern color space */ + public static final int COLORSPACE_PATTERN = 3; + + /** the device-dependent color spaces */ + // private static PDFColorSpace graySpace = + // new PDFColorSpace(ColorSpace.getInstance(ColorSpace.CS_GRAY)); + private static PDFColorSpace rgbSpace = new PDFColorSpace(ColorSpace.getInstance( + ColorSpace.CS_sRGB)); + private static PDFColorSpace cmykSpace = new PDFColorSpace(new CMYKColorSpace()); + + /** the pattern space */ + private static PDFColorSpace patternSpace = new PatternSpace(); + + /** graySpace and the gamma correction for it. */ + private static PDFColorSpace graySpace; + + /** the color space */ + ColorSpace cs; + + /** + * create a PDFColorSpace based on a Java ColorSpace + * @param cs the Java ColorSpace + */ + public PDFColorSpace(ColorSpace cs) { + this.cs = cs; + } + + /** + * Get a color space by name + * + * @param name the name of one of the device-dependent color spaces + */ + public static PDFColorSpace getColorSpace(int name) { + switch (name) { + case COLORSPACE_GRAY: + return rgbSpace; + case COLORSPACE_RGB: + return rgbSpace; + + case COLORSPACE_CMYK: + return cmykSpace; + + case COLORSPACE_PATTERN: + return patternSpace; + + default: + throw new IllegalArgumentException("Unknown Color Space name: " + + name); + } + } + + /** + * Get a color space specified in a PDFObject + * + * @param csobj the PDFObject with the colorspace information + */ + public static PDFColorSpace getColorSpace(PDFObject csobj, Map resources) + throws IOException { + String name; + + PDFObject colorSpaces = null; + + if (resources != null) { + colorSpaces = (PDFObject) resources.get("ColorSpace"); + } + + if (csobj.getType() == PDFObject.NAME) { + name = csobj.getStringValue(); + + if (name.equals("DeviceGray") || name.equals("G")) { + return getColorSpace(COLORSPACE_GRAY); + } else if (name.equals("DeviceRGB") || name.equals("RGB")) { + return getColorSpace(COLORSPACE_RGB); + } else if (name.equals("DeviceCMYK") || name.equals("CMYK")) { + return getColorSpace(COLORSPACE_CMYK); + } else if (name.equals("Pattern")) { + return getColorSpace(COLORSPACE_PATTERN); + } else if (colorSpaces != null) { + csobj = colorSpaces.getDictRef(name); + } + } + + if (csobj == null) { + return null; + } else if (csobj.getCache() != null) { + return (PDFColorSpace) csobj.getCache(); + } + + PDFColorSpace value = null; + + // csobj is [/name <>] + PDFObject[] ary = csobj.getArray(); + name = ary[0].getStringValue(); + + if (name.equals("DeviceGray") || name.equals("G")) { + return getColorSpace(COLORSPACE_GRAY); + } else if (name.equals("DeviceRGB") || name.equals("RGB")) { + return getColorSpace(COLORSPACE_RGB); + } else if (name.equals("DeviceCMYK") || name.equals("CMYK")) { + return getColorSpace(COLORSPACE_CMYK); + } else if (name.equals("CalGray")) { + value = new PDFColorSpace(new CalGrayColor(ary[1])); + } else if (name.equals("CalRGB")) { + value = new PDFColorSpace(new CalRGBColor(ary[1])); + } else if (name.equals("Lab")) { + value = new PDFColorSpace(new LabColor(ary[1])); + } else if (name.equals("ICCBased")) { + try { + ByteArrayInputStream bais = new ByteArrayInputStream(ary[1].getStream()); + ICC_Profile profile = ICC_Profile.getInstance(bais); + if (profile.getColorSpaceType() == ColorSpace.CS_GRAY || profile.getColorSpaceType() == ColorSpace.TYPE_GRAY) { + return graySpace; + } + value = new PDFColorSpace(new ICC_ColorSpace(profile)); + } catch (IllegalArgumentException e) { + return getColorSpace(COLORSPACE_RGB); + } + } else if (name.equals("Separation") || name.equals("DeviceN")) { + PDFColorSpace alternate = getColorSpace(ary[2], resources); + PDFFunction function = PDFFunction.getFunction(ary[3]); + + value = new AlternateColorSpace(alternate, function); + } else if (name.equals("Indexed") || name.equals("I")) { + /** + * 4.5.5 [/Indexed baseColor hival lookup] + */ + PDFColorSpace refspace = getColorSpace(ary[1], resources); + + // number of indices= ary[2], data is in ary[3]; + int count = ary[2].getIntValue(); + try { + value = new IndexedColor(refspace, count, ary[3]); + }catch(Exception e) { + // there might be problems in reading the colorspace from stream, + // in that case use the reference colorspace + value = refspace; + } + + } else if (name.equals("Pattern")) { + if (ary.length == 1) { + return getColorSpace(COLORSPACE_PATTERN); + } + + PDFColorSpace base = getColorSpace(ary[1], resources); + + return new PatternSpace(base); + } else if(name.equals("DeviceRGB")) { + return getColorSpace(COLORSPACE_RGB); + } else if(name.equals("DeviceCMYK")) { + return getColorSpace(COLORSPACE_CMYK); + } else { + // removed access to ary[1] dur to index out of bounds exceptions + throw new PDFParseException("Unknown color space: " + name); + } + + csobj.setCache(value); + + return value; + } + + /** + * get the number of components expected in the getPaint command + */ + public int getNumComponents() { + return this.cs.getNumComponents(); + } + + /** + * get the PDFPaint representing the color described by the + * given color components + * @param components the color components corresponding to the given + * colorspace + * @return a PDFPaint object representing the closest Color to the + * given components. + */ + public PDFPaint getPaint(float[] components) { + float[] rgb = this.cs.toRGB(components); + + return PDFPaint.getColorPaint(new Color(rgb[0], rgb[1], rgb[2])); + } + + /** + * get the original Java ColorSpace. + */ + public ColorSpace getColorSpace() { + return this.cs; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/PatternSpace.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/PatternSpace.java new file mode 100644 index 0000000000..7c6b2c7706 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/PatternSpace.java @@ -0,0 +1,107 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.colorspace; + +import java.io.IOException; +import java.util.Map; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFPaint; +import com.github.librepdf.pdfrenderer.pattern.PDFPattern; + +/** + * A PatternSpace fills with a pattern, the name of which is + * specified in the call to getPaint(). This pattern is + * read from the resources of the current page. The pattern space + * may also have a base color space which the pattern is defined in. + */ +public class PatternSpace extends PDFColorSpace { + private PDFColorSpace base; + + public PatternSpace() { + super(null); + } + + /** + * Create a pattern space with the given color space as a base + */ + public PatternSpace(PDFColorSpace base) { + super(null); + + this.base = base; + } + + /** + * Get the base color space + */ + public PDFColorSpace getBase() { + return this.base; + } + + /** + * Get the number of components we want + */ + @Override public int getNumComponents() { + if (this.base == null) { + return 0; + } else { + return this.base.getNumComponents(); + } + } + + /** + * get the PDFPaint representing the color described by the + * given color components + * @param components the color components corresponding to the given + * colorspace + * @return a PDFPaint object representing the closest Color to the + * given components. + */ + @Override public PDFPaint getPaint(float[] components) { + throw new IllegalArgumentException("Pattern spaces require a pattern " + + "name!"); + } + + /** + * Get the paint representing a pattern, optionally with the given + * base paint. + * + * @param patternObj the pattern to render + * @param components the components of the base paint + */ + public PDFPaint getPaint(PDFObject patternObj, float[] components, + Map resources) + throws IOException + { + PDFPaint basePaint = null; + + if (getBase() != null) { + basePaint = getBase().getPaint(components); + } + + PDFPattern pattern = (PDFPattern) patternObj.getCache(); + if (pattern == null) { + pattern = PDFPattern.getPattern(patternObj, resources); + patternObj.setCache(pattern); + } + + return pattern.getPaint(basePaint); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/YCCKColorSpace.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/YCCKColorSpace.java new file mode 100644 index 0000000000..50fabff055 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/YCCKColorSpace.java @@ -0,0 +1,133 @@ +/* + * Copyright 2008 Pirion Systems Pty Ltd, 139 Warry St, + * Fortitude Valley, Queensland, Australia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.colorspace; + +import java.awt.color.ColorSpace; + +/** + * A ColorSpace for the YCCK color space. This color space converts to CMYK and then + * uses an existing CMYK color space to convert from CMYK to RGB. This allows embedded + * CMYK color profiles to be used with YCCK images. If no CMYK color space is + * provided then by default it uses a CMYKColorSpace. Only toRGB is supported. + * + * @author Ben Day + */ +public class YCCKColorSpace extends ColorSpace +{ + + private final ColorSpace cmykColorSpace; + + /** + * create a new YCCK color space: a ColorSpace with 4 components + */ + public YCCKColorSpace(ColorSpace existingCmykColorSpace) + { + super(TYPE_4CLR, 4); + cmykColorSpace = existingCmykColorSpace; + } + + public YCCKColorSpace() + { + this(new CMYKColorSpace()); + } + + /** + * Convert from CIEXYZ to RGB. NOT IMPLEMENTED + */ + @Override + public float[] fromCIEXYZ(float[] colorvalue) + { + throw new UnsupportedOperationException("Not yet implemented"); + } + + /** + * Convert from RGB to YCCK. NOT IMPLEMENTED + * + * @param rgbvalue the red, green, and blue values (0-1) + * @return the YCCK values (0-1) + */ + @Override + public float[] fromRGB(float[] rgbvalue) + { + throw new UnsupportedOperationException("Not yet implemented"); + } + + /** + * the number of components + */ + @Override + public int getNumComponents() + { + return 4; + } + + /** + * the name of this color space + */ + @Override + public String getName(int idx) + { + return "YCCK"; + } + + /** + * the type of this color space (TYPE_4CLR) + */ + @Override + public int getType() + { + return TYPE_4CLR; + } + + /** + * Convert from YCCK to CIEXYZ. NOT IMPLEMENTED + */ + @Override + public float[] toCIEXYZ(float[] colorvalue) + { + return cmykColorSpace.toCIEXYZ(toCmyk(colorvalue)); + } + + /** + * Convert from YCCK to RGB. + * + * @param colorvalue the YCCK values (0-1) + * @return the RGB values (0-1) + */ + @Override + public float[] toRGB(float[] colorvalue) + { + return cmykColorSpace.toRGB(toCmyk(colorvalue)); + } + + private float[] toCmyk(float[] colorvalue) { + float y = colorvalue[0], cb = colorvalue[1], cr = colorvalue[2], k = colorvalue[3]; + float[] cmyk = new float[4]; + float v; + v = (float) (1.0 - (y + 1.402 * (cr - 0.5))); + cmyk[0] = v < 0.0f ? 0.0f : (v > 1.0f ? 1.0f : v); + v = (float) (1.0 - (y - 0.34414 * (cb - 0.5) - 0.71414 * (cr - 0.5))); + cmyk[1] = v < 0.0f ? 0.0f : (v > 1.0f ? 1.0f : v); + v = (float) (1.0 - (y + 1.772 * (cb - 0.5))); + cmyk[2] = v < 0.0f ? 0.0f : (v > 1.0f ? 1.0f : v); + cmyk[3] = k; + return cmyk; + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/sGray.icc b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/sGray.icc new file mode 100644 index 0000000000..0ac8e07534 Binary files /dev/null and b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/colorspace/sGray.icc differ diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/ASCII85Decode.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/ASCII85Decode.java new file mode 100644 index 0000000000..aee628448f --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/ASCII85Decode.java @@ -0,0 +1,150 @@ +/* Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.decode; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; + +import com.github.librepdf.pdfrenderer.PDFFile; +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/** + * decode ASCII85 text into a byte array. + * + * @author Mike Wessler + */ +public class ASCII85Decode { + + private ByteBuffer buf; + + /** + * initialize the decoder with byte buffer in ASCII85 format + */ + private ASCII85Decode(ByteBuffer buf) { + this.buf = buf; + } + + /** + * get the next character from the input. + * @return the next character, or -1 if at end of stream + */ + private int nextChar() { + // skip whitespace + // returns next character, or -1 if end of stream + while (this.buf.remaining() > 0) { + char c = (char) this.buf.get(); + + if (!PDFFile.isWhiteSpace(c)) { + return c; + } + } + + // EOF reached + return -1; + } + + /** + * decode the next five ASCII85 characters into up to four decoded + * bytes. Return false when finished, or true otherwise. + * + * @param baos the ByteArrayOutputStream to write output to, set to the + * correct position + * @return false when finished, or true otherwise. + */ + private boolean decode5(ByteArrayOutputStream baos) + throws PDFParseException { + // stream ends in ~> + int[] five = new int[5]; + int i; + for (i = 0; i < 5; i++) { + five[i] = nextChar(); + if (five[i] == '~') { + if (nextChar() == '>') { + break; + } else { + throw new PDFParseException("Bad character in ASCII85Decode: not ~>"); + } + } else if (five[i] >= '!' && five[i] <= 'u') { + five[i] -= '!'; + } else if (five[i] == 'z') { + if (i == 0) { + five[i] = 0; + i = 4; + } else { + throw new PDFParseException("Inappropriate 'z' in ASCII85Decode"); + } + } else { + throw new PDFParseException("Bad character in ASCII85Decode: " + five[i] + " (" + (char) five[i] + ")"); + } + } + + if (i > 0) { + i -= 1; + } + + int value = + five[0] * 85 * 85 * 85 * 85 + + five[1] * 85 * 85 * 85 + + five[2] * 85 * 85 + + five[3] * 85 + + five[4]; + + for (int j = 0; j < i; j++) { + int shift = 8 * (3 - j); + baos.write((byte) ((value >> shift) & 0xff)); + } + + return (i == 4); + } + + /** + * decode the bytes + * @return the decoded bytes + */ + private ByteBuffer decode() throws PDFParseException { + // start from the beginning of the data + this.buf.rewind(); + + // allocate the output buffer + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + // decode the bytes + while (decode5(baos)) { + } + + return ByteBuffer.wrap(baos.toByteArray()); + } + + /** + * decode an array of bytes in ASCII85 format. + *

    + * In ASCII85 format, every 5 characters represents 4 decoded + * bytes in base 85. The entire stream can contain whitespace, + * and ends in the characters '~>'. + * + * @param buf the encoded ASCII85 characters in a byte buffer + * @param params parameters to the decoder (ignored) + * @return the decoded bytes + */ + public static ByteBuffer decode(ByteBuffer buf, PDFObject params) + throws PDFParseException { + ASCII85Decode me = new ASCII85Decode(buf); + return me.decode(); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/ASCIIHexDecode.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/ASCIIHexDecode.java new file mode 100644 index 0000000000..b71baac08c --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/ASCIIHexDecode.java @@ -0,0 +1,126 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.decode; + +import com.github.librepdf.pdfrenderer.PDFFile; +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; + +/** + * decode an array of hex nybbles into a byte array + * + * @author Mike Wessler + */ +public class ASCIIHexDecode { + private ByteBuffer buf; + + /** + * initialize the decoder with an array of bytes in ASCIIHex format + */ + private ASCIIHexDecode(ByteBuffer buf) { + this.buf = buf; + } + + /** + * get the next character from the input + * @return a number from 0-15, or -1 for the end character + */ + private int readHexDigit() throws PDFParseException { + // read until we hit a non-whitespace character or the + // end of the stream + while (this.buf.remaining() > 0) { + int c = this.buf.get(); + + // see if we found a useful character + if (!PDFFile.isWhiteSpace((char) c)) { + if (c >= '0' && c <= '9') { + c -= '0'; + } else if (c >= 'a' && c <= 'f') { + c -= 'a' - 10; + } else if (c >= 'A' && c <= 'F') { + c -= 'A' - 10; + } else if (c == '>') { + c = -1; + } else { + // unknown character + throw new PDFParseException("Bad character " + c + + "in ASCIIHex decode"); + } + + // return the useful character + return c; + } + } + + // end of stream reached + throw new PDFParseException("Short stream in ASCIIHex decode"); + } + + /** + * decode the array + * @return the decoded bytes + */ + private ByteBuffer decode() throws PDFParseException { + // start at the beginning of the buffer + buf.rewind(); + + // allocate the output buffer + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + while (true) { + int first = readHexDigit(); + if (first == -1) { + break; + } + int second = readHexDigit(); + + if (second == -1) { + baos.write((byte) (first << 4)); + break; + } else { + baos.write((byte) ((first << 4) + second)); + } + } + + return ByteBuffer.wrap(baos.toByteArray()); + } + + /** + * decode an array of bytes in ASCIIHex format. + *

    + * ASCIIHex format consists of a sequence of Hexidecimal + * digits, with possible whitespace, ending with the + * '>' character. + * + * @param buf the encoded ASCII85 characters in a byte + * buffer + * @param params parameters to the decoder (ignored) + * @return the decoded bytes + */ + public static ByteBuffer decode(ByteBuffer buf, PDFObject params) + throws PDFParseException + { + ASCIIHexDecode me = new ASCIIHexDecode(buf); + return me.decode(); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/CCITTCodes b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/CCITTCodes new file mode 100644 index 0000000000..10c1c419cd --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/CCITTCodes @@ -0,0 +1,236 @@ + +# $Id: CCITTCodes,v 1.2 2007/12/20 18:33:33 rbair Exp $ +# +# Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, +# Santa Clara, California 95054, U.S.A. All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + +# WHITE CODES +00110101 0 +000111 1 +0111 2 +1000 3 +1011 4 +1100 5 +1110 6 +1111 7 +10011 8 +10100 9 +00111 10 +01000 11 +001000 12 +000011 13 +110100 14 +110101 15 +101010 16 +101011 17 +0100111 18 +0001100 19 +0001000 20 +0010111 21 +0000011 22 +0000100 23 +0101000 24 +0101011 25 +0010011 26 +0100100 27 +0011000 28 +00000010 29 +00000011 30 +00011010 31 +00011011 32 +00010010 33 +00010011 34 +00010100 35 +00010101 36 +00010110 37 +00010111 38 +00101000 39 +00101001 40 +00101010 41 +00101011 42 +00101100 43 +00101101 44 +00000100 45 +00000101 46 +00001010 47 +00001011 48 +01010010 49 +01010011 50 +01010100 51 +01010101 52 +00100100 53 +00100101 54 +01011000 55 +01011001 56 +01011010 57 +01011011 58 +01001010 59 +01001011 60 +00110010 61 +00110011 62 +00110100 63 +11011 64 +10010 128 +010111 192 +0110111 256 +00110110 320 +00110111 384 +01100100 448 +01100101 512 +01101000 576 +01100111 640 +011001100 704 +011001101 768 +011010010 832 +011010011 896 +011010100 960 +011010101 1024 +011010110 1088 +011010111 1152 +011011000 1216 +011011001 1280 +011011010 1344 +011011011 1408 +010011000 1472 +010011001 1536 +010011010 1600 +011000 1664 +010011011 1728 +00000001000 1792 +00000001100 1856 +00000001101 1920 +000000010010 1984 +000000010011 2048 +000000010100 2112 +000000010101 2176 +000000010110 2240 +000000010111 2304 +000000011100 2368 +000000011101 2432 +000000011110 2496 +000000011111 2560 +000000001111 -2 +0000000000 -1 + +# BLACK CODES +0000110111 0 +010 1 +11 2 +10 3 +011 4 +0011 5 +0010 6 +00011 7 +000101 8 +000100 9 +0000100 10 +0000101 11 +0000111 12 +00000100 13 +00000111 14 +000011000 15 +0000010111 16 +0000011000 17 +0000001000 18 +00001100111 19 +00001101000 20 +00001101100 21 +00000110111 22 +00000101000 23 +00000010111 24 +00000011000 25 +000011001010 26 +000011001011 27 +000011001100 28 +000011001101 29 +000001101000 30 +000001101001 31 +000001101010 32 +000001101011 33 +000011010010 34 +000011010011 35 +000011010100 36 +000011010101 37 +000011010110 38 +000011010111 39 +000001101100 40 +000001101101 41 +000011011010 42 +000011011011 43 +000001010100 44 +000001010101 45 +000001010110 46 +000001010111 47 +000001100100 48 +000001100101 49 +000001010010 50 +000001010011 51 +000000100100 52 +000000110111 53 +000000111000 54 +000000100111 55 +000000101000 56 +000001011000 57 +000001011001 58 +000000101011 59 +000000101100 60 +000001011010 61 +000001100110 62 +000001100111 63 +0000001111 64 +000011001000 128 +000011001001 192 +000001011011 256 +000000110011 320 +000000110100 384 +000000110101 448 +0000001101100 512 +0000001101101 576 +0000001001010 640 +0000001001011 704 +0000001001100 768 +0000001001101 832 +0000001110010 896 +0000001110011 960 +0000001110100 1024 +0000001110101 1088 +0000001110110 1152 +0000001110111 1216 +0000001010010 1280 +0000001010011 1344 +0000001010100 1408 +0000001010101 1472 +0000001011010 1536 +0000001011011 1600 +0000001100100 1664 +0000001100101 1728 +00000001000 1792 +00000001100 1856 +00000001101 1920 +000000010010 1984 +000000010011 2048 +000000010100 2112 +000000010101 2176 +000000010110 2240 +000000010111 2304 +000000011100 2368 +000000011101 2432 +000000011110 2496 +000000011111 2560 +000000001111 -2 +00000000000 -1 diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/CCITTFaxDecode.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/CCITTFaxDecode.java new file mode 100644 index 0000000000..0a3a8b4654 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/CCITTFaxDecode.java @@ -0,0 +1,117 @@ +package com.github.librepdf.pdfrenderer.decode; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import com.github.librepdf.pdfrenderer.PDFDebugger; +import com.github.librepdf.pdfrenderer.PDFObject; + +public class CCITTFaxDecode { + + + + protected static ByteBuffer decode(PDFObject dict, ByteBuffer buf, + PDFObject params) throws IOException { + + byte[] bytes = new byte[buf.remaining()]; + buf.get(bytes, 0, bytes.length); + return ByteBuffer.wrap(decode(dict, bytes)); + } + + + protected static byte[] decode(PDFObject dict, byte[] source) throws IOException { + int width = 1728; + PDFObject widthDef = dict.getDictRef("Width"); + if (widthDef == null) { + widthDef = dict.getDictRef("W"); + } + if (widthDef != null) { + width = widthDef.getIntValue(); + } + int height = 0; + PDFObject heightDef = dict.getDictRef("Height"); + if (heightDef == null) { + heightDef = dict.getDictRef("H"); + } + if (heightDef != null) { + height = heightDef.getIntValue(); + } + + // + int columns = getOptionFieldInt(dict, "Columns", width); + int rows = getOptionFieldInt(dict, "Rows", height); + int k = getOptionFieldInt(dict, "K", 0); + int size = rows * ((columns + 7) >> 3); + byte[] destination = new byte[size]; + + boolean align = getOptionFieldBoolean(dict, "EncodedByteAlign", false); + + CCITTFaxDecoder decoder = new CCITTFaxDecoder(1, columns, rows); + decoder.setAlign(align); + try { + if (k == 0) { + decoder.decodeT41D(destination, source, 0, rows); + } else if (k > 0) { + decoder.decodeT42D(destination, source, 0, rows); + } else if (k < 0) { + decoder.decodeT6(destination, source, 0, rows); + } + }catch (Exception e) { + PDFDebugger.debug("Error decoding CCITTFax image k: "+ k); + // some PDf producer don't correctly assign a k value for the deocde, + // as result we can try one more time using the T6. + //first, reset buffer + destination = new byte[size]; + try { + decoder.decodeT6(destination, source, 0, rows); + }catch (Exception e1) { + // do nothing + PDFDebugger.debug("Error decoding CCITTFax image"); + } + } + if (!getOptionFieldBoolean(dict, "BlackIs1", false)) { + for (int i = 0; i < destination.length; i++) { + // bitwise not + destination[i] = (byte) ~destination[i]; + } + } + + return destination; + } + + public static int getOptionFieldInt(PDFObject dict, String name, int defaultValue) throws IOException { + + PDFObject dictParams = getDecodeParams(dict); + + if (dictParams == null) { + return defaultValue; + } + PDFObject value = dictParams.getDictRef(name); + if (value == null) { + return defaultValue; + } + return value.getIntValue(); + } + + public static boolean getOptionFieldBoolean(PDFObject dict, String name, boolean defaultValue) throws IOException { + + PDFObject dictParams = getDecodeParams(dict); + + if (dictParams == null) { + return defaultValue; + } + PDFObject value = dictParams.getDictRef(name); + if (value == null) { + return defaultValue; + } + return value.getBooleanValue(); + } + + private static PDFObject getDecodeParams(PDFObject dict) throws IOException { + PDFObject decdParams = dict.getDictRef("DecodeParms"); + if (decdParams != null && decdParams.getType() == PDFObject.ARRAY) { + return decdParams.getArray()[0]; + } + return decdParams; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/CCITTFaxDecoder.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/CCITTFaxDecoder.java new file mode 100644 index 0000000000..381485c569 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/CCITTFaxDecoder.java @@ -0,0 +1,1581 @@ +/* + * Based on the SUN code (see license beyond) changes are made to handle CCITTFax encoded + * data in a PDF image. This may or may not apply to real world CCITT documents. + * + * Copyright (c) 2007, intarsys consulting GmbH + * + * 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 intarsys 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. + */ +/* + * Copyright (c) 2001 Sun Microsystems, Inc. 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. + * + * -Redistribution in binary form must reproduct 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 Sun Microsystems, Inc. or the names of contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING ANY + * IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR + * NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN AND ITS LICENSORS SHALL NOT BE + * LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING + * OR DISTRIBUTING THE SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR ITS + * LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, + * INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER + * CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF + * OR INABILITY TO USE SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that Software is not designed,licensed or intended for use in + * the design, construction, operation or maintenance of any nuclear facility. + */ +package com.github.librepdf.pdfrenderer.decode; + +public class CCITTFaxDecoder { + 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, }; + + private int bitPointer; + + private int bytePointer; + + private byte[] data; + + private int w; + + private boolean align = false; + + private int fillOrder; + + // Data structures needed to store changing elements for the previous + // and the current scanline + private int changingElemSize = 0; + + private int[] prevChangingElems; + + private int[] currChangingElems; + + // Element at which to start search in getNextChangingElement + private int lastChangingElement = 0; + + private boolean fillBits = false; + + /** + * @param fillOrder + * The fill order of the compressed data bytes. + * @param w + * @param h + */ + public CCITTFaxDecoder(int fillOrder, int w, int h) { + this.fillOrder = fillOrder; + // Some of the decode methods assume prevChangingElms + // and currChaningElems are at least of length 2. + if(w<2) { + w=2; + } + + this.w = w; + + this.bitPointer = 0; + this.bytePointer = 0; + this.prevChangingElems = new int[w]; + this.currChangingElems = new int[w]; + } + + private boolean align() { + if (this.align && this.bitPointer != 0) { + this.bytePointer++; + this.bitPointer = 0; + return true; + } + return false; + } + + protected boolean consumeEOL() { + // Get the next 12 bits. + int next12Bits = nextNBits(12); + if (next12Bits == 1) { + // EOL found & consumed + return true; + } + // no EOL - unread and return + updatePointer(12); + return false; + } + + // 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( + "EOL code word encountered in Black run."); //$NON-NLS-1$ + } 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; + } + + protected 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 + this.changingElemSize = 0; + + // While scanline not complete + while (bitOffset < this.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("Invalid code encountered."); + } else if (bits == 15) { + // EOL recover + // move bits back... + updatePointer(10); + return; + } else { + // 11 bits - 0000 0111 1111 1111 = 0x07ff + code = (entry >>> 5) & 0x07ff; + bitOffset += code; + + updatePointer(10 - bits); + if (isT == 0) { + isWhite = false; + this.currChangingElems[this.changingElemSize++] = bitOffset; + } + } + } + + // Check whether this run completed one width, if so + // advance to next byte boundary for compression = 2. + if (bitOffset == this.w) { + align(); + break; + } + + while (isWhite == false) { + // 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 recover + // unread bits ??? + updatePointer(9); + return; + } else { + setToBlack(buffer, lineOffset, bitOffset, code); + bitOffset += code; + + updatePointer(9 - bits); + if (isT == 0) { + isWhite = true; + this.currChangingElems[this.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; + this.currChangingElems[this.changingElemSize++] = bitOffset; + } else { + // Is a Terminating code + setToBlack(buffer, lineOffset, bitOffset, code); + bitOffset += code; + + updatePointer(4 - bits); + isWhite = true; + this.currChangingElems[this.changingElemSize++] = bitOffset; + } + } + + // Check whether this run completed one width + if (bitOffset == this.w) { + align(); + break; + } + } + + this.currChangingElems[this.changingElemSize++] = bitOffset; + } + + // One-dimensional decoding methods + public void decodeT41D(byte[] buffer, byte[] compData, int startX, + int height) { + this.data = compData; + int scanlineStride = (this.w + 7) / 8; + this.bitPointer = 0; + this.bytePointer = 0; + + int lineOffset = 0; + for (int i = 0; i < height; i++) { + consumeEOL(); + decodeNextScanline(buffer, lineOffset, startX); + lineOffset += scanlineStride; + } + } + + // Two-dimensional decoding methods + public void decodeT42D(byte[] buffer, byte[] compData, int startX, + int height) { + this.data = compData; + int scanlineStride = (this.w + 7) / 8; + this.bitPointer = 0; + this.bytePointer = 0; + + 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; + + // The data must start with an EOL code + if (readEOL(true) != 1) { + throw new RuntimeException("First scanline must be 1D encoded."); //$NON-NLS-1$ + } + + 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(false) == 0) { + // 2D encoded scanline follows + + // Initialize previous scanlines changing elements, and + // initialize current scanline's changing elements array + temp = this.prevChangingElems; + this.prevChangingElems = this.currChangingElems; + this.currChangingElems = temp; + currIndex = 0; + + // a0 has to be set just before the start of this scanline. + a0 = -1; + isWhite = true; + bitOffset = startX; + + this.lastChangingElement = 0; + + while (bitOffset < this.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 = (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; + this.currChangingElems[currIndex++] = bitOffset; + + number = decodeBlackCodeWord(); + setToBlack(buffer, lineOffset, bitOffset, number); + bitOffset += number; + this.currChangingElems[currIndex++] = bitOffset; + } else { + number = decodeBlackCodeWord(); + setToBlack(buffer, lineOffset, bitOffset, number); + bitOffset += number; + this.currChangingElems[currIndex++] = bitOffset; + + number = decodeWhiteCodeWord(); + bitOffset += number; + this.currChangingElems[currIndex++] = bitOffset; + } + + a0 = bitOffset; + } else if (code <= 8) { + // Vertical + a1 = b1 + (code - 5); + + this.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( + "Invalid code encountered while decoding 2D group 3 compressed data."); //$NON-NLS-1$ + } + } + + // Add the changing element beyond the current scanline for the + // other color too + this.currChangingElems[currIndex++] = bitOffset; + this.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) { + this.data = compData; + int scanlineStride = (this.w + 7) / 8; + this.bitPointer = 0; + this.bytePointer = 0; + + 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 + + // Local cached reference + int[] cce = this.currChangingElems; + + // Assume invisible preceding row of all white pixels and insert + // both black and white changing elements beyond the end of this + // imaginary scanline. + this.changingElemSize = 0; + cce[this.changingElemSize++] = this.w; + cce[this.changingElemSize++] = this.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 = this.prevChangingElems; + this.prevChangingElems = this.currChangingElems; + cce = this.currChangingElems = temp; + currIndex = 0; + + // Start decoding the scanline at startX in the raster + bitOffset = startX; + + // Reset search start position for getNextChangingElement + this.lastChangingElement = 0; + + // Till one whole scanline is decoded + while (bitOffset < this.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 = (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) { + if (b2 > this.w) { + b2 = this.w; + } + 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(); + if (number > this.w - bitOffset) { + number = this.w - bitOffset; + } + setToBlack(buffer, lineOffset, bitOffset, number); + bitOffset += number; + cce[currIndex++] = bitOffset; + } else { + // First a black run and then a white run follows + number = decodeBlackCodeWord(); + if (number > this.w - bitOffset) { + number = this.w - bitOffset; + } + 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) { + if (a1 > this.w) { + a1 = this.w; + } + 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( + "Invalid code encountered while decoding 2D group 4 compressed data."); //$NON-NLS-1$ + } + + 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 { + // break line - seems to be a common failure + // unread + updatePointer(7 - bits); + // and mark lines as complete + bitOffset = this.w; + // throw new RuntimeException( + // "Invalid code encountered while decoding 2D group 4 + // compressed data."); //$NON-NLS-1$ + } + } + + align(); + + // Add the changing element beyond the current scanline for the + // other color too + // make sure that the index does not exceed the bounds of the array + if (currIndex < this.w) { + cce[currIndex++] = bitOffset; + } + + // Number of changing elements in this scanline. + this.changingElemSize = currIndex; + + lineOffset += scanlineStride; + } + } + + // 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("Invalid code encountered."); //$NON-NLS-1$ + } else if (bits == 15) { // EOL + throw new RuntimeException( + "EOL code word encountered in White run."); //$NON-NLS-1$ + } else { + // 11 bits - 0000 0111 1111 1111 = 0x07ff + code = (entry >>> 5) & 0x07ff; + runLength += code; + updatePointer(10 - bits); + if (isT == 0) { + isWhite = false; + } + } + } + + return runLength; + } + + 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 = (this.lastChangingElement > 0) ? (this.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) { + this.lastChangingElement = i; + ret[0] = temp; + break; + } + } + + if ((i + 1) < ces) { + ret[1] = pce[i + 1]; + } + } + + public boolean isAlign() { + return this.align; + } + + public boolean isFillBits() { + return this.fillBits; + } + + private int nextLesserThan8Bits(int bitsToGet) { + byte b; + byte next; + int l = this.data.length - 1; + int bp = this.bytePointer; + + if (this.fillOrder == 1) { + b = this.data[bp]; + if (bp == l) { + next = 0x00; + } else { + next = this.data[bp + 1]; + } + } else if (this.fillOrder == 2) { + b = flipTable[this.data[bp] & 0xff]; + if (bp == l) { + next = 0x00; + } else { + next = flipTable[this.data[bp + 1] & 0xff]; + } + } else { + throw new RuntimeException("tag must be either 1 or 2."); //$NON-NLS-1$ + } + + int bitsLeft = 8 - this.bitPointer; + int bitsFromNextByte = bitsToGet - bitsLeft; + + int shift = bitsLeft - bitsToGet; + int i1; + int i2; + if (shift >= 0) { + i1 = (b & table1[bitsLeft]) >>> shift; + this.bitPointer += bitsToGet; + if (this.bitPointer == 8) { + this.bitPointer = 0; + this.bytePointer++; + } + } else { + i1 = (b & table1[bitsLeft]) << (-shift); + i2 = (next & table2[bitsFromNextByte]) >>> (8 - bitsFromNextByte); + + i1 |= i2; + this.bytePointer++; + this.bitPointer = bitsFromNextByte; + } + + return i1; + } + + private int nextNBits(int bitsToGet) { + byte b; + byte next; + byte next2next; + int l = this.data.length - 1; + int bp = this.bytePointer; + + if (this.fillOrder == 1) { + b = this.data[bp]; + + if (bp == l) { + next = 0x00; + next2next = 0x00; + } else if ((bp + 1) == l) { + next = this.data[bp + 1]; + next2next = 0x00; + } else { + next = this.data[bp + 1]; + next2next = this.data[bp + 2]; + } + } else if (this.fillOrder == 2) { + b = flipTable[this.data[bp] & 0xff]; + + if (bp == l) { + next = 0x00; + next2next = 0x00; + } else if ((bp + 1) == l) { + next = flipTable[this.data[bp + 1] & 0xff]; + next2next = 0x00; + } else { + next = flipTable[this.data[bp + 1] & 0xff]; + next2next = flipTable[this.data[bp + 2] & 0xff]; + } + } else { + throw new RuntimeException("tag must be either 1 or 2."); //$NON-NLS-1$ + } + + int bitsLeft = 8 - this.bitPointer; + int bitsFromNextByte = bitsToGet - bitsLeft; + int bitsFromNext2NextByte = 0; + if (bitsFromNextByte > 8) { + bitsFromNext2NextByte = bitsFromNextByte - 8; + bitsFromNextByte = 8; + } + + this.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; + this.bytePointer++; + this.bitPointer = bitsFromNext2NextByte; + } else { + if (bitsFromNextByte == 8) { + this.bitPointer = 0; + this.bytePointer++; + } else { + this.bitPointer = bitsFromNextByte; + } + } + + int i = i1 | i2; + return i; + } + + private int readEOL(boolean isFirstEOL) { + // Seek to the next EOL. + if (!seekEOL()) { + throw new RuntimeException("EOL not found"); + } + + if (!this.fillBits) { + int next12Bits = nextNBits(12); + if (isFirstEOL && (next12Bits == 0)) { + // Might have the case of EOL padding being used even + // though it was not flagged. + // This was observed to be the case in TIFFs produced + // by a well known vendor who shall remain nameless. + if (nextNBits(4) == 1) { + // EOL must be padded: reset the fillBits flag. + this.fillBits = true; + return 1; + } + } + if (next12Bits != 1) { + throw new RuntimeException( + "Scanline must begin with EOL code word."); //$NON-NLS-1$ + } + } else { + // 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 - this.bitPointer; + + if (nextNBits(bitsLeft) != 0) { + throw new RuntimeException( + "All fill bits preceding EOL code must be 0."); //$NON-NLS-1$ + } + + // 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( + "All fill bits preceding EOL code must be 0."); //$NON-NLS-1$ + } + } + + // + // Some encoders under Group 3 Fax compression 1D writes TIFF + // files without the fill bits, but say otherwise. + // Need to check for this here. + // + int next8 = nextNBits(8); + + if (isFirstEOL && (next8 & 0xf0) == 0x10) { + // + // Fill bits are not actually used despite what the flag + // says. So switch fillBits off and then rewind so that + // only 12 bits have effectively been read. + // + this.fillBits = false; + updatePointer(4); + } else { + // + // This is the normal case. + // 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. + // + while (next8 != 1) { + // If not all zeros + if (next8 != 0) { + throw new RuntimeException("0 bits expected before EOL"); + } + next8 = nextNBits(8); + } + } + } + // The next one bit signifies 1D/2D encoding of next line. + return nextLesserThan8Bits(1); + } + + // Seeks to the next EOL in the compressed bitstream. + // Returns 'true' if and only if an EOL is found; if 'false' + // is returned it may be inferred that the EOF was reached first. + private boolean seekEOL() { + // Set maximum and current bit index into the compressed data. + int bitIndexMax = this.data.length * 8 - 1; + int bitIndex = this.bytePointer * 8 + this.bitPointer; + + // Loop while at least 12 bits are available. + while (bitIndex <= bitIndexMax - 12) { + // Get the next 12 bits. + int next12Bits = nextNBits(12); + bitIndex += 12; + + // Loop while the 12 bits are not unity, i.e., while the EOL + // has not been reached, and there is at least one bit left. + while (next12Bits != 1 && bitIndex < bitIndexMax) { + next12Bits = ((next12Bits & 0x000007ff) << 1) + | (nextLesserThan8Bits(1) & 0x00000001); + bitIndex++; + } + + // If EOL reached, rewind the pointers and return 'true'. + if (next12Bits == 1) { + updatePointer(12); + return true; + } + } + + // EOL not found: return 'false'. + return false; + } + + public void setAlign(boolean align) { + this.align = align; + } + + public void setFillBits(boolean fillBits) { + this.fillBits = fillBits; + } + + 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; + } + } + + // Move pointer backwards by given amount of bits + private void updatePointer(int bitsToMoveBack) { + if (bitsToMoveBack > 8) { + this.bytePointer -= bitsToMoveBack / 8; + bitsToMoveBack %= 8; + } + + int i = this.bitPointer - bitsToMoveBack; + + if (i < 0) { + this.bytePointer--; + this.bitPointer = 8 + i; + } else { + this.bitPointer = i; + } + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/DCTDecode.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/DCTDecode.java new file mode 100644 index 0000000000..026c014e17 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/DCTDecode.java @@ -0,0 +1,128 @@ +package com.github.librepdf.pdfrenderer.decode; + +import java.awt.Image; +import java.awt.Toolkit; +import java.awt.image.BufferedImage; +import java.awt.image.ImageObserver; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +import javax.imageio.ImageIO; +import javax.swing.ImageIcon; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/** + * Decode a DCT encoded array into a byte array. This class uses Java's + * built-in JPEG image class to do the decoding. + * + * @author Mike Wessler + */ +public class DCTDecode { + + /** + * Decode an array of bytes in DCT format. + *

    + * DCT is the format used by JPEG images, so this class simply + * loads the DCT-format bytes as an image, then reads the bytes out + * of the image to create the array. Unfortunately, their most + * likely use is to get turned BACK into an image, so this isn't + * terribly efficient... but is general... don't hit, please. + *

    + * The DCT-encoded stream may have 1, 3, or 4 samples per pixel, depending + * on the colorspace of the image. In decoding, we look for the colorspace + * in the stream object's dictionary to decide how to decode this image. + * If no colorspace is present, we guess 3 samples per pixel. + * + * @param dict the stream dictionary + * @param buf the DCT-encoded buffer + * @param params the parameters to the decoder (ignored) + * @return the decoded buffer + * @throws PDFParseException + */ + protected static ByteBuffer decode(PDFObject dict, ByteBuffer buf, PDFObject params) throws PDFParseException { + BufferedImage bimg = loadImageData(buf); + byte[] output = ImageDataDecoder.decodeImageData(bimg); + return ByteBuffer.wrap(output); + } + + /** + * Load image data from the buffer. + * + * @param buf the buffer containing the image data + * @return a BufferedImage representing the image + * @throws PDFParseException if an error occurs during image loading + */ + private static BufferedImage loadImageData(ByteBuffer buf) throws PDFParseException { + buf.rewind(); + byte[] input = new byte[buf.remaining()]; + buf.get(input); + BufferedImage bimg; + try (ByteArrayInputStream bais = new ByteArrayInputStream(input)) { + try { + bimg = ImageIO.read(bais); + if (bimg == null) { + throw new PDFParseException("DCTDecode failed: ImageIO.read returned null"); + } + } catch (IOException ex) { + // If there's an issue reading the image, attempt to load it another way + Image img = Toolkit.getDefaultToolkit().createImage(input); + // Wait until the image is fully loaded + ImageIcon imageIcon = new ImageIcon(img); + // Copy to buffered image + bimg = new BufferedImage(imageIcon.getIconWidth(), imageIcon.getIconHeight(), BufferedImage.TYPE_INT_RGB); + bimg.getGraphics().drawImage(img, 0, 0, null); + } + } catch (Exception ex) { + PDFParseException ex2 = new PDFParseException("DCTDecode failed"); + ex2.initCause(ex); + throw ex2; + } + + return bimg; + } +} + +/** + * Image tracker. This class tracks the status of an image loading operation. + */ +class MyTracker implements ImageObserver { + boolean done = false; + + /** + * Create a new MyTracker that watches this image. The image will start loading immediately. + */ + public MyTracker(Image img) { + img.getWidth(this); + } + + /** + * More information has come in about the image. + */ + @Override + public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { + if ((infoflags & (ALLBITS | ERROR | ABORT)) != 0) { + synchronized (this) { + this.done = true; + notifyAll(); + } + return false; + } + return true; + } + + /** + * Wait until the image is done, then return. + */ + public synchronized void waitForAll() { + if (!this.done) { + try { + wait(); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + } + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/FlateDecode.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/FlateDecode.java new file mode 100644 index 0000000000..16b4d0623d --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/FlateDecode.java @@ -0,0 +1,96 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.decode; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/** + * decode a deFlated byte array + * @author Mike Wessler + */ +public class FlateDecode { + + /** + * decode a byte buffer in Flate format. + *

    + * Flate is a built-in Java algorithm. It's part of the java.util.zip + * package. + * + * @param buf the deflated input buffer + * @param params parameters to the decoder (unused) + * @return the decoded (inflated) bytes + */ + public static ByteBuffer decode(PDFObject dict, ByteBuffer buf, + PDFObject params) throws IOException { + Inflater inf = new Inflater(false); + + int bufSize = buf.remaining(); + + // copy the data, since the array() method is not supported + // on raf-based ByteBuffers + byte[] data = new byte[bufSize]; + buf.get(data); + + // set the input to the inflater + inf.setInput(data); + + // output to a byte-array output stream, since we don't + // know how big the output will be + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] decomp = new byte[bufSize]; + int read = 0; + + try { + while (!inf.finished()) { + read = inf.inflate(decomp); + if (read <= 0) { + if (inf.needsDictionary()) { + throw new PDFParseException("Don't know how to ask for a dictionary in FlateDecode"); + } else { + // just return the data which is already read + break; + } + } + baos.write(decomp, 0, read); + } + } catch (DataFormatException dfe) { + throw new PDFParseException("Data format exception:" + dfe.getMessage()); + } + + // return the output as a byte buffer + ByteBuffer outBytes = ByteBuffer.wrap(baos.toByteArray()); + + // undo a predictor algorithm, if any was used + if (params != null && params.getDictionary().containsKey("Predictor")) { + Predictor predictor = Predictor.getPredictor(params); + if (predictor != null) { + outBytes = predictor.unpredict(outBytes); + } + } + + return outBytes; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/ImageDataDecoder.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/ImageDataDecoder.java new file mode 100644 index 0000000000..b56623e873 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/ImageDataDecoder.java @@ -0,0 +1,80 @@ +package com.github.librepdf.pdfrenderer.decode; + +import java.awt.Graphics; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; +import java.awt.image.DataBufferInt; + +/***************************************************************************** + * Decode image data to a usable color space. + * + * @since 25.03.2011 + ****************************************************************************/ + +public class ImageDataDecoder { + + /************************************************************************* + * @param bimg + * @return + ************************************************************************/ + + static byte[] decodeImageData(BufferedImage bimg) { + byte[] output = null; + + int type = bimg.getType(); + + if (type == BufferedImage.TYPE_INT_RGB) { + // read back the data + DataBufferInt db = (DataBufferInt) bimg.getData() + .getDataBuffer(); + int[] data = db.getData(); + + output = new byte[data.length * 3]; + for (int i = 0, offset = 0; i < data.length; i++, offset += 3) { + output[offset] = (byte) (data[i] >> 16); + output[offset + 1] = (byte) (data[i] >> 8); + output[offset + 2] = (byte) (data[i]); + } + } else if (type == BufferedImage.TYPE_BYTE_GRAY) { + DataBufferByte db = (DataBufferByte) bimg.getData() + .getDataBuffer(); + output = db.getData(); + } else if (type == BufferedImage.TYPE_INT_ARGB) { + // read back the data + DataBufferInt db = (DataBufferInt) bimg.getData() + .getDataBuffer(); + int[] data = db.getData(); + + output = new byte[data.length * 4]; + for (int i = 0, offset = 0; i < data.length; i++, offset += 4) { + output[offset] = (byte) (data[i] >> 24); + output[offset + 1] = (byte) (data[i] >> 16); + output[offset + 2] = (byte) (data[i] >> 8); + output[offset + 3] = (byte) (data[i]); + } + } else { + // The raster is in some other format. + // We have to convert it into TYPE_INT_RGB before we can use it. + BufferedImage tmp = new BufferedImage(bimg.getWidth(), + bimg.getHeight(), BufferedImage.TYPE_INT_RGB); + Graphics g = tmp.createGraphics(); + g.drawImage(bimg, 0, 0, null); + g.dispose(); + // read back the data + DataBufferInt db = (DataBufferInt) tmp.getData() + .getDataBuffer(); + int[] data = db.getData(); + + output = new byte[data.length * 3]; + for (int i = 0, offset = 0; i < data.length; i++, offset += 3) { + output[offset] = (byte) (data[i] >> 16); + output[offset + 1] = (byte) (data[i] >> 8); + output[offset + 2] = (byte) (data[i]); + } + tmp.flush(); + } + return output; + } + + +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/JPXDecode.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/JPXDecode.java new file mode 100644 index 0000000000..5ddd549eb1 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/JPXDecode.java @@ -0,0 +1,86 @@ +/* Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.decode; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Iterator; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.MemoryCacheImageInputStream; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/** + * decode a JPX encoded imagestream into a byte array. This class uses Java's + * image_io JPEG2000 reader to do the decoding. + * + * @author Bernd Rosstauscher + */ + +public class JPXDecode { + + /************************************************************************* + * @param dict + * @param buf + * @param params + * @return + * @throws PDFParseException + ************************************************************************/ + + protected static ByteBuffer decode(PDFObject dict, ByteBuffer buf, PDFObject params) throws PDFParseException { + BufferedImage bimg = loadImageData(buf); + byte[] output = ImageDataDecoder.decodeImageData(bimg); + return ByteBuffer.wrap(output); + } + + /************************************************************************* + * @param buf + * @return + * @throws PDFParseException + * @throws IOException + ************************************************************************/ + + private static BufferedImage loadImageData(ByteBuffer buf) throws PDFParseException { + ImageReader reader = null; + try { + byte[] input = new byte[buf.remaining()]; + buf.get(input); + Iterator readers = ImageIO.getImageReadersByMIMEType("image/jpeg2000"); + if (readers.hasNext() == false) { + throw new PDFParseException("JPXDecode failed. No reader available"); + } + reader = readers.next(); + reader.setInput(new MemoryCacheImageInputStream(new ByteArrayInputStream(input))); + BufferedImage bimg = reader.read(0); + return bimg; + } catch (IOException e) { + throw new PDFParseException("JPXDecode failed", e); + } finally { + if (reader != null) { + reader.dispose(); + } + } + + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/LZWDecode.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/LZWDecode.java new file mode 100644 index 0000000000..6cf7083909 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/LZWDecode.java @@ -0,0 +1,201 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.decode; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/** + * decode an LZW-encoded array of bytes. LZW is a patented algorithm. + * + *

    Feb 21, 2009 Legal statement on Intellectual Property from Unisys

    + * LZW Patent Information (http://www.unisys.com/about__unisys/lzw)
    + * License Information on GIF and Other LZW-based Technologies
    + * 

    + * Unisys U.S. LZW Patent No. 4,558,302 expired on June 20, 2003, + * the counterpart patents in the United Kingdom, France, Germany and + * Italy expired on June 18, 2004, the Japanese counterpart patents + * expired on June 20, 2004 and the counterpart Canadian patent + * expired on July 7, 2004. + *

    + * Unisys Corporation holds and has patents pending on a number of + * improvements on the inventions claimed in the above-expired patents. + * Information on these improvement patents and terms under which they + * may be licensed can be obtained by contacting the following: + *

    + * Unisys Corporation + * Welch Patent Licensing Department + * Mail Stop E8-114 + * Unisys Way + * Blue Bell, PA 19424 + *

    + * Via the Internet, send email to Robert.Marley@unisys.com. + *

    + * Via facsimile, send inquiries to Welch Patent Licensing Department at + * 215-986-3090. + *

    + * The above is presented for information purposes only, and is subject + * to change by Unisys. Additionally, this information should not be + * considered as legally obligating Unisys in any way with regard to license + * availability, or as to the terms and conditions offered for a license, + * or with regard to the interpretation of any license agreements. + * You should consult with your own legal counsel regarding your + * particular situation. + *

    + * + * @author Mike Wessler + */ +public class LZWDecode { + + ByteBuffer buf; + int bytepos; + int bitpos; + byte[] dict[] = new byte[4096][]; + int dictlen = 0; + int bitspercode = 9; + static int STOP = 257; + static int CLEARDICT = 256; + + /** + * initialize this decoder with an array of encoded bytes + * @param buf the buffer of bytes + */ + private LZWDecode(ByteBuffer buf) throws PDFParseException { + for (int i = 0; i < 256; i++) { + this.dict[i] = new byte[1]; + this.dict[i][0] = (byte) i; + } + this.dictlen = 258; + this.bitspercode = 9; + this.buf = buf; + this.bytepos = 0; + this.bitpos = 0; + } + + /** + * reset the dictionary to the initial 258 entries + */ + private void resetDict() { + this.dictlen = 258; + this.bitspercode = 9; + } + + /** + * get the next code from the input stream + */ + private int nextCode() { + int fillbits = this.bitspercode; + int value = 0; + if (this.bytepos >= this.buf.limit() - 1) { + return -1; + } + while (fillbits > 0) { + int nextbits = this.buf.get(this.bytepos); // bitsource + int bitsfromhere = 8 - this.bitpos; // how many bits can we take? + if (bitsfromhere > fillbits) { // don't take more than we need + bitsfromhere = fillbits; + } + value |= ((nextbits >> (8 - this.bitpos - bitsfromhere)) & + (0xff >> (8 - bitsfromhere))) << (fillbits - bitsfromhere); + fillbits -= bitsfromhere; + this.bitpos += bitsfromhere; + if (this.bitpos >= 8) { + this.bitpos = 0; + this.bytepos++; + } + } + return value; + } + + /** + * decode the array. + * @return the uncompressed byte array + */ + private ByteBuffer decode() throws PDFParseException { + // algorithm derived from: + // http://www.rasip.fer.hr/research/compress/algorithms/fund/lz/lzw.html + // and the PDFReference + int cW = CLEARDICT; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + while (true) { + int pW = cW; + cW = nextCode(); + if (cW == -1) { + throw new PDFParseException("Missed the stop code in LZWDecode!"); + } + if (cW == STOP) { + break; + } else if (cW == CLEARDICT) { + resetDict(); + // pW= -1; + } else if (pW == CLEARDICT) { + baos.write(this.dict[cW], 0, this.dict[cW].length); + } else { + if (cW < this.dictlen) { // it's a code in the dictionary + baos.write(this.dict[cW], 0, this.dict[cW].length); + byte[] p = new byte[this.dict[pW].length + 1]; + System.arraycopy(this.dict[pW], 0, p, 0, this.dict[pW].length); + p[this.dict[pW].length] = this.dict[cW][0]; + this.dict[this.dictlen++] = p; + } else { // not in the dictionary (should==dictlen) + // if (cW!=dictlen) { + // System.out.println("Got a bouncy code: "+cW+" (dictlen="+dictlen+")"); + // } + byte[] p = new byte[this.dict[pW].length + 1]; + System.arraycopy(this.dict[pW], 0, p, 0, this.dict[pW].length); + p[this.dict[pW].length] = p[0]; + baos.write(p, 0, p.length); + this.dict[this.dictlen++] = p; + } + if (this.dictlen >= (1 << this.bitspercode) - 1 && this.bitspercode < 12) { + this.bitspercode++; + } + } + } + return ByteBuffer.wrap(baos.toByteArray()); + } + + /** + * decode an array of LZW-encoded bytes to a byte array. + * + * @param buf the buffer of encoded bytes + * @param params parameters for the decoder (unused) + * @return the decoded uncompressed bytes + */ + public static ByteBuffer decode(ByteBuffer buf, PDFObject params) + throws IOException { + // decode the array + LZWDecode me = new LZWDecode(buf); + ByteBuffer outBytes = me.decode(); + + // undo a predictor algorithm, if any was used + if (params != null && params.getDictionary().containsKey("Predictor")) { + Predictor predictor = Predictor.getPredictor(params); + if (predictor != null) { + outBytes = predictor.unpredict(outBytes); + } + } + + return outBytes; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/PDFDecoder.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/PDFDecoder.java new file mode 100644 index 0000000000..056143f606 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/PDFDecoder.java @@ -0,0 +1,214 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.decode; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import com.github.librepdf.pdfrenderer.decrypt.PDFDecrypterFactory; +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/** + * A PDF Decoder encapsulates all the methods of decoding a stream of bytes + * based on all the various encoding methods. + *

    + * You should use the decodeStream() method of this object rather than using + * any of the decoders directly. + */ +public class PDFDecoder { + + public final static Set DCT_FILTERS = new HashSet(Arrays.asList("DCT", "DCTDecode")); + + /** Creates a new instance of PDFDecoder */ + private PDFDecoder() { + } + + public static boolean isLastFilter(PDFObject dict, Set filters) throws IOException { + PDFObject filter = dict.getDictRef("Filter"); + if (filter == null) { + return false; + } else if (filter.getType() == PDFObject.NAME) { + return filters.contains(filter.getStringValue()); + } else { + final PDFObject[] ary = filter.getArray(); + return filters.contains(ary[ary.length - 1].getStringValue()); + } + } + + /** + * Utility class for reading and storing the specification of + * Filters on a stream + */ + private static class FilterSpec + { + PDFObject ary[]; + PDFObject params[]; + + private FilterSpec(PDFObject dict, PDFObject filter) throws IOException { + if (filter.getType() == PDFObject.NAME) { + ary = new PDFObject[1]; + ary[0] = filter; + params = new PDFObject[1]; + params[0] = dict.getDictRef("DecodeParms"); + } else { + ary = filter.getArray(); + PDFObject parmsobj = dict.getDictRef("DecodeParms"); + if (parmsobj != null) { + params = parmsobj.getArray(); + } else { + params = new PDFObject[ary.length]; + } + } + } + + } + + /** + * decode a byte[] stream using the filters specified in the object's + * dictionary (passed as argument 1). + * @param dict the dictionary associated with the stream + * @param streamBuf the data in the stream, as a byte buffer + */ + public static ByteBuffer decodeStream(PDFObject dict, ByteBuffer streamBuf, Set filterLimits) + throws IOException { + + PDFObject filter = dict.getDictRef("Filter"); + if (filter == null) { + // just apply default decryption + return dict.getDecrypter().decryptBuffer(null, dict, streamBuf); + } else { + // apply filters + FilterSpec spec = new FilterSpec(dict, filter); + + // determine whether default encryption applies or if there's a + // specific Crypt filter; it must be the first filter according to + // the errata for PDF1.7 + boolean specificCryptFilter = + spec.ary.length != 0 && spec.ary[0].getStringValue().equals("Crypt"); + if (!specificCryptFilter) { + // No Crypt filter, so should apply default decryption (if + // present!) + streamBuf = dict.getDecrypter().decryptBuffer( + null, dict, streamBuf); + } + + for (int i = 0; i < spec.ary.length; i++) { + String enctype = spec.ary[i].getStringValue(); + try { + if (filterLimits.contains(enctype)) { + break; + } + if (enctype == null) { + } else if (enctype.equals("FlateDecode") || enctype.equals("Fl")) { + streamBuf = FlateDecode.decode(dict, streamBuf, spec.params[i]); + } else if (enctype.equals("LZWDecode") || enctype.equals("LZW")) { + streamBuf = LZWDecode.decode(streamBuf, spec.params[i]); + } else if (enctype.equals("ASCII85Decode") || enctype.equals("A85")) { + streamBuf = ASCII85Decode.decode(streamBuf, spec.params[i]); + } else if (enctype.equals("ASCIIHexDecode") || enctype.equals("AHx")) { + streamBuf = ASCIIHexDecode.decode(streamBuf, spec.params[i]); + } else if (enctype.equals("RunLengthDecode") || enctype.equals("RL")) { + streamBuf = RunLengthDecode.decode(streamBuf, spec.params[i]); + } else if (enctype.equals("DCTDecode") || enctype.equals("DCT")) { + streamBuf = DCTDecode.decode(dict, streamBuf, spec.params[i]); + } else if (enctype.equals("JPXDecode")) { + streamBuf = JPXDecode.decode(dict, streamBuf, spec.params[i]); + } else if (enctype.equals("CCITTFaxDecode") || enctype.equals("CCF")) { + streamBuf = CCITTFaxDecode.decode(dict, streamBuf, spec.params[i]); + } else if (enctype.equals("Crypt")) { + String cfName = PDFDecrypterFactory.CF_IDENTITY; + if (spec.params[i] != null) { + final PDFObject nameObj = spec.params[i].getDictRef("Name"); + if (nameObj != null && nameObj.getType() == PDFObject.NAME) { + cfName = nameObj.getStringValue(); + } + } + streamBuf = dict.getDecrypter().decryptBuffer(cfName, null, streamBuf); + } else { + throw new PDFParseException("Unknown coding method:" + spec.ary[i].getStringValue()); + } + }catch(Exception e) { + throw new PDFParseException("Problem decoding "+enctype+" encoded stream!", e); + } + + } + } + + return streamBuf; + } + + /** + * The name of the Crypt filter to apply + * @param param the parameters to the Crypt filter + * @return the name of the crypt filter to apply + * @throws IOException if there's a problem reading the objects + */ + private static String getCryptFilterName(PDFObject param) throws IOException { + String cfName = PDFDecrypterFactory.CF_IDENTITY; + if (param != null) { + final PDFObject nameObj = param.getDictRef("Name"); + if (nameObj != null && nameObj.getType() == PDFObject.NAME) { + cfName = nameObj.getStringValue(); + } + } + return cfName; + } + + /** + * Determines whether a stream is encrypted or not; note that encodings + * (e.g., Flate, LZW) are not considered encryptions. + * @param dict the stream dictionary + * @return whether the stream is encrypted + * @throws IOException if the stream dictionary can't be read + */ + public static boolean isEncrypted(PDFObject dict) + throws IOException { + + PDFObject filter = dict.getDictRef("Filter"); + if (filter == null) { + // just apply default decryption + return dict.getDecrypter().isEncryptionPresent(); + } else { + + // apply filters + FilterSpec spec = new FilterSpec(dict, filter); + + // determine whether default encryption applies or if there's a + // specific Crypt filter; it must be the first filter according to + // the errata for PDF1.7 + boolean specificCryptFilter = spec.ary.length != 0 && + spec.ary[0].getStringValue().equals("Crypt"); + if (!specificCryptFilter) { + // No Crypt filter, so we just need to refer to + // the default decrypter + return dict.getDecrypter().isEncryptionPresent(); + } else { + String cfName = getCryptFilterName(spec.params[0]); + // see whether the specified crypt filter really decrypts + return dict.getDecrypter().isEncryptionPresent(cfName); + } + } + + } + +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/PNGPredictor.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/PNGPredictor.java new file mode 100644 index 0000000000..8c5f40c052 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/PNGPredictor.java @@ -0,0 +1,212 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.decode; + +import java.nio.ByteBuffer; + +import java.io.IOException; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Undo prediction based on the PNG algorithm. + */ +public class PNGPredictor extends Predictor { + /** Creates a new instance of PNGPredictor */ + public PNGPredictor() { + super (PNG); + } + + /** + * Undo data based on the png algorithm + */ + @Override + public ByteBuffer unpredict(ByteBuffer imageData) + throws IOException + { + List rows = new ArrayList(); + + byte[] curLine = null; + byte[] prevLine = null; + + // get the number of bytes per row + int rowSize = getColumns() * getColors() * getBitsPerComponent(); + rowSize = (int) Math.ceil(rowSize / 8.0); + + while(imageData.remaining() >= rowSize + 1) { + // the first byte determines the algorithm + int algorithm = (imageData.get() & 0xff); + + // read the rest of the line + curLine = new byte[rowSize]; + imageData.get(curLine); + + // use the algorithm, Luke + switch (algorithm) { + case 0: + // none + break; + case 1: + doSubLine(curLine); + break; + case 2: + doUpLine(curLine, prevLine); + break; + case 3: + doAverageLine(curLine, prevLine); + break; + case 4: + doPaethLine(curLine, prevLine); + break; + } + + rows.add(curLine); + prevLine = curLine; + } + + // turn into byte array + ByteBuffer outBuf = ByteBuffer.allocate(rows.size() * rowSize); + for (Iterator i = rows.iterator(); i.hasNext();) { + outBuf.put((byte[]) i.next()); + } + + // reset start pointer + outBuf.flip(); + + // return + return outBuf; + + } + + /** + * Return the value of the Sub algorithm on the line (compare bytes to + * the previous byte of the same color on this line). + */ + protected void doSubLine(byte[] curLine) { + // get the number of bytes per sample + int sub = (int) Math.ceil((getBitsPerComponent() * getColors()) / 8.0); + + for (int i = 0; i < curLine.length; i++) { + int prevIdx = i - sub; + if (prevIdx >= 0) { + curLine[i] += curLine[prevIdx]; + } + } + } + + /** + * Return the value of the up algorithm on the line (compare bytes to + * the same byte in the previous line) + */ + protected void doUpLine(byte[] curLine, byte[] prevLine) { + if (prevLine == null) { + // do nothing if this is the first line + return; + } + + for (int i = 0; i < curLine.length; i++) { + curLine[i] += prevLine[i]; + } + } + + /** + * Return the value of the average algorithm on the line (compare + * bytes to the average of the previous byte of the same color and + * the same byte on the previous line) + */ + protected void doAverageLine(byte[] curLine, byte[] prevLine) { + // get the number of bytes per sample + int sub = (int) Math.ceil((getBitsPerComponent() * getColors()) / 8.0); + + for (int i = 0; i < curLine.length; i++) { + int raw = 0; + int prior = 0; + + // get the last value of this color + int prevIdx = i - sub; + if (prevIdx >= 0) { + raw = curLine[prevIdx] & 0xff; + } + + // get the value on the previous line + if (prevLine != null) { + prior = prevLine[i] & 0xff; + } + + // add the average + curLine[i] += (byte) Math.floor((raw + prior) / 2); + } + } + + /** + * Return the value of the average algorithm on the line (compare + * bytes to the average of the previous byte of the same color and + * the same byte on the previous line) + */ + protected void doPaethLine(byte[] curLine, byte[] prevLine) { + // get the number of bytes per sample + int sub = (int) Math.ceil((getBitsPerComponent() * getColors()) / 8.0); + + for (int i = 0; i < curLine.length; i++) { + int left = 0; + int up = 0; + int upLeft = 0; + + // get the last value of this color + int prevIdx = i - sub; + if (prevIdx >= 0) { + left = curLine[prevIdx] & 0xff; + } + + // get the value on the previous line + if (prevLine != null) { + up = prevLine[i] & 0xff; + } + + if (prevIdx >= 0 && prevLine != null) { + upLeft = prevLine[prevIdx] & 0xff; + } + + // add the average + curLine[i] += (byte) paeth(left, up, upLeft); + } + } + + /** + * The paeth algorithm + */ + protected int paeth(int left, int up, int upLeft) { + int p = left + up - upLeft; + int pa = Math.abs(p - left); + int pb = Math.abs(p - up); + int pc = Math.abs(p - upLeft); + + if ((pa <= pb) && (pa <= pc)) { + return left; + } else if (pb <= pc) { + return up; + } else { + return upLeft; + } + } + +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/Predictor.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/Predictor.java new file mode 100644 index 0000000000..97965fce63 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/Predictor.java @@ -0,0 +1,173 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.decode; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/** + * The abstract superclass of various predictor objects that undo well-known + * prediction algorithms. + */ +public abstract class Predictor { + /** well known algorithms */ + public static final int TIFF = 0; + public static final int PNG = 1; + + /** the algorithm to use */ + private final int algorithm; + + /** the number of colors per sample */ + private int colors = 1; + + /** the number of bits per color component */ + private int bpc = 8; + + /** the number of columns per row */ + private int columns = 1; + + /** + * Create an instance of a predictor. Use getPredictor() + * instead of this. + */ + protected Predictor(int algorithm) { + this.algorithm = algorithm; + } + + /** + * Actually perform this algorithm on decoded image data. + * Subclasses must implement this method + */ + public abstract ByteBuffer unpredict(ByteBuffer imageData) + throws IOException; + + /** + * Get an instance of a predictor + * + * @param params the filter parameters + */ + public static Predictor getPredictor(PDFObject params) + throws IOException + { + // get the algorithm (required) + PDFObject algorithmObj = params.getDictRef("Predictor"); + if (algorithmObj == null) { + // no predictor + return null; + } + int algorithm = algorithmObj.getIntValue(); + + // create the predictor object + Predictor predictor = null; + switch (algorithm) { + case 1: + // no predictor + return null; + case 2: + predictor = new TIFFPredictor(); + break; + case 10: + case 11: + case 12: + case 13: + case 14: + case 15: + predictor = new PNGPredictor(); + break; + default: + throw new PDFParseException("Unknown predictor: " + algorithm); + } + + // read the colors (optional) + PDFObject colorsObj = params.getDictRef("Colors"); + if (colorsObj != null) { + predictor.setColors(colorsObj.getIntValue()); + } + + // read the bits per component (optional) + PDFObject bpcObj = params.getDictRef("BitsPerComponent"); + if (bpcObj != null) { + predictor.setBitsPerComponent(bpcObj.getIntValue()); + } + + // read the columns (optional) + PDFObject columnsObj = params.getDictRef("Columns"); + if (columnsObj != null) { + predictor.setColumns(columnsObj.getIntValue()); + } + + // all set + return predictor; + } + + /** + * Get the algorithm in use + * + * @return one of the known algorithm types + */ + public int getAlgorithm() { + return this.algorithm; + } + + /** + * Get the number of colors per sample + */ + public int getColors() { + return this.colors; + } + + /** + * Set the number of colors per sample + */ + protected void setColors(int colors) { + this.colors = colors; + } + + /** + * Get the number of bits per color component + */ + public int getBitsPerComponent() { + return this.bpc; + } + + /** + * Set the number of bits per color component + */ + public void setBitsPerComponent(int bpc) { + this.bpc = bpc; + } + + /** + * Get the number of columns + */ + public int getColumns() { + return this.columns; + } + + /** + * Set the number of columns + */ + public void setColumns(int columns) { + this.columns = columns; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/RunLengthDecode.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/RunLengthDecode.java new file mode 100644 index 0000000000..23414e162a --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/RunLengthDecode.java @@ -0,0 +1,94 @@ +/* + * Copyright 2009 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.decode; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/** + * decode an array of Run Length encoded bytes into a byte array + * + * @author Mike Wessler + */ +public class RunLengthDecode { + /** the end of data in the RunLength encoding. */ + private static final int RUN_LENGTH_EOD = 128; + + private ByteBuffer buf; + + /** + * initialize the decoder with an array of bytes in RunLength format + */ + private RunLengthDecode(ByteBuffer buf) { + this.buf = buf; + } + + /** + * decode the array + * + * @return the decoded bytes + */ + private ByteBuffer decode() { + // start at the beginning of the buffer + this.buf.rewind(); + + // allocate the output buffer + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + int dupAmount; + byte[] buffer = new byte[128]; + while ((dupAmount = this.buf.get()&0xFF) != RUN_LENGTH_EOD) { + if (dupAmount >= 0 && dupAmount <= 127) { + int amountToCopy = dupAmount + 1; + this.buf.get(buffer, 0, amountToCopy); + baos.write(buffer, 0, amountToCopy); + } else { + byte dupByte = this.buf.get(); + for (int i = 0; i < 257 - dupAmount; i++) { + baos.write(dupByte); + } + } + } + return ByteBuffer.wrap(baos.toByteArray()); + } + + /** + * decode an array of bytes in RunLength format. + *

    + * RunLength format consists of a sequence of a byte-oriented format based + * on run length. There are a series of "runs", where a run is a length byte + * followed by 1 to 128 bytes of data. If the length is 0-127, the following + * length+1 (1 to 128) bytes are to be copied. If the length is 129 through + * 255, the following single byte is copied 257-length (2 to 128) times. A + * length value of 128 means and End of Data (EOD). + * + * @param buf + * the RUnLEngth encoded bytes in a byte buffer + * + * @param params + * parameters to the decoder (ignored) + * @return the decoded bytes + */ + public static ByteBuffer decode(ByteBuffer buf, PDFObject params) throws PDFParseException { + RunLengthDecode me = new RunLengthDecode(buf); + return me.decode(); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/TIFFPredictor.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/TIFFPredictor.java new file mode 100644 index 0000000000..5e245cadbb --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decode/TIFFPredictor.java @@ -0,0 +1,123 @@ +/* + * $Id: TIFFPredictor.java,v 1.1 2010-05-23 22:07:04 lujke Exp $ + * + * Copyright 2010 Pirion Systems Pty Ltd, 139 Warry St, + * Fortitude Valley, Queensland, Australia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.decode; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Undo prediction based on the TIFF Predictor 2 algorithm + */ +public class TIFFPredictor extends Predictor { + + public TIFFPredictor() { + super (TIFF); + } + + /** + * Undo data based on the png algorithm + */ + public ByteBuffer unpredict(ByteBuffer imageData) + throws IOException + { + ByteBuffer out = ByteBuffer.allocate(imageData.limit()); + + final int numComponents = getColors(); + final int pixelBits = numComponents * getBitsPerComponent(); + + int bytePerRow = (getColumns() * pixelBits + 7) / 8; + + final byte[] row = new byte[bytePerRow]; + + while(imageData.remaining() > 0) { + imageData.get(row); + if (getBitsPerComponent() == 8) { + for (int i = numComponents; i < row.length; i += numComponents) { + for (int c = 0; c < numComponents; ++c) { + final int pos = i + c; + row[pos] += row[pos - numComponents]; + } + } + } else if (getBitsPerComponent() == 16) { + final short[] prev = new short[numComponents]; + for (int c = 0; c < numComponents; c += 1) { + final int pos = c * 2; + prev[c] = (short) ((row[pos] << 8 | (row[pos + 1]) & 0xFFFF)); + } + for (int i = numComponents * 2; i < row.length; i += numComponents * 2) { + for (int c = 0; c < numComponents; c += 1) { + final int pos = i + c * 2; + short cur = (short) ((row[pos] << 8 | (row[pos + 1]) & 0xFFFF)); + cur += prev[c]; + row[pos] = (byte) (cur >>> 8 & 0xFF); + row[pos + 1] = (byte) (cur & 0xFF); + prev[c] = cur; + } + } + } else { + assert getBitsPerComponent() == 1 || getBitsPerComponent() == 2 || getBitsPerComponent() == 4 : "we don't want to grab components across pixel boundaries"; + int bitsOnRow = pixelBits * getColumns(); // may be less than bytesOnRow * 8 + byte prev[] = new byte[numComponents]; + final int shiftWhenAligned = 8 - getBitsPerComponent(); + final int mask = (1 << getBitsPerComponent()) - 1; + for (int c = 0; c < numComponents; ++c) { + prev[c] = getbits(row, c * getBitsPerComponent(), shiftWhenAligned, mask); + } + for (int i = pixelBits; i < bitsOnRow; i += pixelBits) { + for (int c = 0; c < numComponents; ++c) { + byte cur = getbits(row, i + c * getBitsPerComponent(), shiftWhenAligned, mask); + cur += prev[c]; + prev[c] = cur; + setbits(row, i + c * getBitsPerComponent(), shiftWhenAligned, mask, cur); + } + } + } + out.put(row); + } + + + // reset start pointer + out.flip(); + + // return + return out; + + } + + private static byte getbits(byte[] data, int bitIndex, int shiftWhenByteAligned, int mask) + { + final int b = data[(bitIndex >> 3)]; + final int bitIndexInB = bitIndex & 7; + final int shift = shiftWhenByteAligned - bitIndexInB; + return (byte) ((b >>> shift) & mask); + } + + private static void setbits(byte[] data, int bitIndex, int shiftWhenByteAligned, int mask, byte bits) + { + final int b = data[(bitIndex >> 3)]; + final int bitIndexInB = bitIndex & 7; + final int shift = shiftWhenByteAligned - bitIndexInB; + data[bitIndex >> 3] = (byte) ((b & ~(mask << shift)) | (bits << shift)); + } + + +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/CryptFilterDecrypter.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/CryptFilterDecrypter.java new file mode 100644 index 0000000000..6c91c5e53e --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/CryptFilterDecrypter.java @@ -0,0 +1,139 @@ +/* + * Copyright 2008 Pirion Systems Pty Ltd, 139 Warry St, + * Fortitude Valley, Queensland, Australia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.decrypt; + +import java.nio.ByteBuffer; +import java.util.Map; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/** + * Implements Version 4 standard decryption, whereby the Encrypt dictionary + * contains a list of named 'crypt filters', each of which is the equivalent + * of a {@link PDFDecrypter}. In addition to this list of crypt filters, + * the name of the filter to use for streams and the default filter to use + * for strings is specified. Requests to decode a stream with a named + * decrypter (typically Identity) instead of the default decrypter + * are honoured. + * + * @author Luke Kirby + */ +public class CryptFilterDecrypter implements PDFDecrypter { + + /** Maps from crypt filter names to their corresponding decrypters */ + private Map decrypters; + /** The default decrypter for stream content */ + private PDFDecrypter defaultStreamDecrypter; + /** The default decrypter for string content */ + private PDFDecrypter defaultStringDecrypter; + + /** + * Class constructor + * @param decrypters a map of crypt filter names to their corresponding + * decrypters. Must already contain the Identity filter. + * @param defaultStreamCryptName the crypt filter name of the default + * stream decrypter + * @param defaultStringCryptName the crypt filter name of the default + * string decrypter + * @throws PDFParseException if one of the named defaults is not + * present in decrypters + */ + public CryptFilterDecrypter( + Map decrypters, + String defaultStreamCryptName, + String defaultStringCryptName) + throws PDFParseException { + + this.decrypters = decrypters; + assert this.decrypters.containsKey("Identity") : + "Crypt Filter map does not contain required Identity filter"; + defaultStreamDecrypter = this.decrypters.get(defaultStreamCryptName); + if (defaultStreamDecrypter == null) { + throw new PDFParseException( + "Unknown crypt filter specified as default for streams: " + + defaultStreamCryptName); + } + defaultStringDecrypter = this.decrypters.get(defaultStringCryptName); + if (defaultStringDecrypter == null) { + throw new PDFParseException( + "Unknown crypt filter specified as default for strings: " + + defaultStringCryptName); + } + } + + @Override + public ByteBuffer decryptBuffer( + String cryptFilterName, PDFObject streamObj, ByteBuffer streamBuf) + throws PDFParseException { + final PDFDecrypter decrypter; + if (cryptFilterName == null) { + decrypter = defaultStreamDecrypter; + } else { + decrypter = decrypters.get(cryptFilterName); + if (decrypter == null) { + throw new PDFParseException("Unknown CryptFilter: " + + cryptFilterName); + } + } + return decrypter.decryptBuffer( + // elide the filter name to prevent V2 decrypters from + // complaining about a crypt filter name + null, + // if there's a specific crypt filter being used then objNum + // and objGen shouldn't contribute to the key, so we + // should make sure that no streamObj makes its way through + cryptFilterName != null ? null : streamObj, + streamBuf); + } + + @Override + public String decryptString(int objNum, int objGen, String inputBasicString) + throws PDFParseException { + return defaultStringDecrypter.decryptString(objNum, objGen, inputBasicString); + } + + @Override + public boolean isEncryptionPresent() { + for (final PDFDecrypter decrypter : decrypters.values()) { + if (decrypter.isEncryptionPresent()) { + return true; + } + } + return false; + } + + @Override + public boolean isEncryptionPresent(String cryptFilterName) { + PDFDecrypter decrypter = decrypters.get(cryptFilterName); + return decrypter != null && decrypter.isEncryptionPresent(cryptFilterName); + } + + @Override + public boolean isOwnerAuthorised() { + for (final PDFDecrypter decrypter : decrypters.values()) { + if (decrypter.isOwnerAuthorised()) { + return true; + } + } + return false; + } + +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/EncryptionUnsupportedByPlatformException.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/EncryptionUnsupportedByPlatformException.java new file mode 100644 index 0000000000..0dbb9e9144 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/EncryptionUnsupportedByPlatformException.java @@ -0,0 +1,39 @@ +/* Copyright 2008 Pirion Systems Pty Ltd, 139 Warry St, + * Fortitude Valley, Queensland, Australia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.decrypt; + +/** + * Identifies that the specified encryption mechanism, though supported by the + * product, is not supported by the platform that it is running on; i.e., that + * either the JCE does not support a required cipher or that its policy is + * such that a key of a given length can not be used. + * + * @author Luke Kirby + */ +public class EncryptionUnsupportedByPlatformException + extends UnsupportedEncryptionException { + + public EncryptionUnsupportedByPlatformException(String message) { + super(message); + } + + public EncryptionUnsupportedByPlatformException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/EncryptionUnsupportedByProductException.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/EncryptionUnsupportedByProductException.java new file mode 100644 index 0000000000..8fe14aab3e --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/EncryptionUnsupportedByProductException.java @@ -0,0 +1,36 @@ +/* + * Copyright 2008 Pirion Systems Pty Ltd, 139 Warry St, + * Fortitude Valley, Queensland, Australia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.decrypt; + +/** + * Identifies that the specified encryption mechanism is not + * supported by this product, that is, PDFRenderer, as opposed to + * a {@link EncryptionUnsupportedByPlatformException limitation in + * the platform}. + * + * @author Luke Kirby + */ +public class EncryptionUnsupportedByProductException + extends UnsupportedEncryptionException { + + public EncryptionUnsupportedByProductException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/IdentityDecrypter.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/IdentityDecrypter.java new file mode 100644 index 0000000000..0b4b65ba94 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/IdentityDecrypter.java @@ -0,0 +1,72 @@ +/* + * Copyright 2008 Pirion Systems Pty Ltd, 139 Warry St, + * Fortitude Valley, Queensland, Australia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.decrypt; + +import java.nio.ByteBuffer; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/** + * Performs identity decryption; that is, inputs aren't encrypted and + * are returned right back. + * + * @Author Luke Kirby + */ +public class IdentityDecrypter implements PDFDecrypter { + + private static IdentityDecrypter INSTANCE = new IdentityDecrypter(); + + @Override + public ByteBuffer decryptBuffer(String cryptFilterName, + PDFObject streamObj, ByteBuffer streamBuf) + throws PDFParseException { + + if (cryptFilterName != null) { + throw new PDFParseException("This Encryption version does not support Crypt filters"); + } + + return streamBuf; + } + + @Override + public String decryptString(int objNum, int objGen, String inputBasicString) { + return inputBasicString; + } + + public static IdentityDecrypter getInstance() { + return INSTANCE; + } + + @Override + public boolean isEncryptionPresent() { + return false; + } + + @Override + public boolean isEncryptionPresent(String cryptFilterName) { + return false; + } + + @Override + public boolean isOwnerAuthorised() { + return false; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/PDFAuthenticationFailureException.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/PDFAuthenticationFailureException.java new file mode 100644 index 0000000000..d3ab8885a6 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/PDFAuthenticationFailureException.java @@ -0,0 +1,34 @@ +/* Copyright 2008 Pirion Systems Pty Ltd, 139 Warry St, + * Fortitude Valley, Queensland, Australia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.decrypt; + +import com.github.librepdf.pdfrenderer.PDFParseException; + +/** + * Identifies that the supplied password was incorrect or non-existent + * and required. + * @author Luke Kirby + */ +// TODO - consider having this not extend PDFParseException so that +// it will be handled more explicitly? +public class PDFAuthenticationFailureException extends PDFParseException { + public PDFAuthenticationFailureException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/PDFDecrypter.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/PDFDecrypter.java new file mode 100644 index 0000000000..002453907c --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/PDFDecrypter.java @@ -0,0 +1,104 @@ +/* + * Copyright 2008 Pirion Systems Pty Ltd, 139 Warry St, + * Fortitude Valley, Queensland, Australia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.decrypt; + +import java.nio.ByteBuffer; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; +import com.github.librepdf.pdfrenderer.PDFStringUtil; + +/** + * A decrypter decrypts streams and strings in a PDF document. {@link + * #decryptBuffer(String, PDFObject, ByteBuffer)} } should be used for decoding + * streams, and {@link #decryptString(int, int, String)} for string values in + * the PDF. It is possible for strings and streams to be encrypted with + * different mechanisms, so the appropriate method must alwayus be used. + * + * @see "PDFReference 1.7, Section 3.5 Encryption" + * @author Luke Kirby + */ +public interface PDFDecrypter { + + /** + * Decrypt a buffer of data + * @param cryptFilterName the name of the crypt filter, if V4 + * encryption is being used, where individual crypt filters may + * be specified for individual streams. If encryption is not using + * V4 encryption (indicated by V=4 in the Encrypt dictionary) then + * this must be null. Null may also be specified with V4 encryption + * to indicate that the default filter should be used. + * @param streamObj the object whose stream is being decrypted. The + * containing object's number and generation contribute to the key used for + * stream encrypted with the document's default encryption, so this is + * typically required. Should be null only if a cryptFilterName is + * specified, as objects with specific stream filters use the general + * document key, rather than a stream-specific key. + * @param streamBuf the buffer to decrypt + * @return a buffer containing the decrypted stream, positioned at its + * beginning; will only be the same buffer as streamBuf if the identity + * decrypter is being used + * @throws PDFParseException if the named crypt filter does not exist, or + * if a crypt filter is named when named crypt filters are not supported. + * Problems due to incorrect passwords are revealed prior to this point. + */ + public ByteBuffer decryptBuffer( + String cryptFilterName, + PDFObject streamObj, + ByteBuffer streamBuf) + throws PDFParseException; + + /** + * Decrypt a {@link PDFStringUtil basic string}. + * @param objNum the object number of the containing object + * @param objGen the generation number of the containing object + * @param inputBasicString the string to be decrypted + * @return the decrypted string + * @throws PDFParseException if the named crypt filter does not exist, or + * if a crypt filter is named when named crypt filters are not supported. + * Problems due to incorrect passwords are revealed prior to this point. + */ + public String decryptString(int objNum, int objGen, String inputBasicString) + throws PDFParseException; + + /** + * Determine whether the password known by the decrypter indicates that + * the user is the owner of the document. Can be used, in conjunction + * with {@link #isEncryptionPresent()} to determine whether any + * permissions apply. + * @return whether owner authentication is being used to decrypt the + * document + */ + public boolean isOwnerAuthorised(); + + /** + * Determine whether this actually applies a decryption other than + * identity decryption. + * @return whether encryption is present + */ + public boolean isEncryptionPresent(); + + /** + * Determines whether decryption applies for a given crypt filter name + * @param cryptFilterName the crypt filter name + * @return whether the given crypt filter decrypts or not + */ + boolean isEncryptionPresent(String cryptFilterName); +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/PDFDecrypterFactory.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/PDFDecrypterFactory.java new file mode 100644 index 0000000000..9bfc408039 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/PDFDecrypterFactory.java @@ -0,0 +1,337 @@ +/* Copyright 2008 Pirion Systems Pty Ltd, 139 Warry St, + * Fortitude Valley, Queensland, Australia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.decrypt; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/** + * Produces a {@link PDFDecrypter} for documents given a (possibly non-existent) + * Encrypt dictionary. Supports decryption of versions 1, 2 and 4 of the + * password-based encryption mechanisms as described in PDF Reference version + * 1.7. This means that it supports RC4 and AES encryption with keys of + * 40-128 bits; esentially, password-protected documents with compatibility + * up to Acrobat 8. + * + * @See "PDF Reference version 1.7, section 3.5: Encryption" + * @author Luke Kirby + */ +public class PDFDecrypterFactory { + + /** The name of the standard Identity CryptFilter */ + public static final String CF_IDENTITY = "Identity"; + + /** Default key length for versions where key length is optional */ + private static final int DEFAULT_KEY_LENGTH = 40; + + /** + * Create a decryptor for a given encryption dictionary. A check is + * immediately performed that the supplied password decrypts content + * described by the encryption specification. + * + * @param encryptDict the Encrypt dict as found in the document's trailer. + * May be null, in which case the {@link IdentityDecrypter} will + * be returned. + * @param documentId the object with key "ID" in the trailer's dictionary. + * Should always be present if Encrypt is. + * @param password the password to use; may be null + * @return The decryptor that should be used for all encrypted data in the + * PDF + * @throws IOException will typically be a {@link + * PDFParseException}, indicating an IO problem, an error + * in the structure of the document, or a failure to obtain various ciphers + * from the installed JCE providers + * @throws EncryptionUnsupportedByPlatformException if the encryption + * is not supported by the environment in which the code is executing + * @throws EncryptionUnsupportedByProductException if PDFRenderer does + * not currently support the specified encryption + * @throws PDFAuthenticationFailureException if the supplied password + * was not able to + */ + public static PDFDecrypter createDecryptor + (PDFObject encryptDict, PDFObject documentId, PDFPassword password) + throws + IOException, + EncryptionUnsupportedByPlatformException, + EncryptionUnsupportedByProductException, + PDFAuthenticationFailureException { + + // none of the classes beyond us want to see a null PDFPassword + password = PDFPassword.nonNullPassword(password); + + if (encryptDict == null) { + // No encryption specified + return IdentityDecrypter.getInstance(); + } else { + PDFObject filter = encryptDict.getDictRef("Filter"); + // this means that we'll fail if, for example, public key + // encryption is employed + if (filter != null && "Standard".equals(filter.getStringValue())) { + final PDFObject vObj = encryptDict.getDictRef("V"); + int v = vObj != null ? vObj.getIntValue() : 0; + if (v == 1 || v == 2) { + final PDFObject lengthObj = + encryptDict.getDictRef("Length"); + final Integer length = + lengthObj != null ? lengthObj.getIntValue() : null; + return createStandardDecrypter( + encryptDict, documentId, password, length, false, + StandardDecrypter.EncryptionAlgorithm.RC4); + } else if (v == 4) { + return createCryptFilterDecrypter( + encryptDict, documentId, password, v); + } else { + throw new EncryptionUnsupportedByPlatformException( + "Unsupported encryption version: " + v); + } + } else if (filter == null) { + throw new PDFParseException( + "No Filter specified in Encrypt dictionary"); + } else { + throw new EncryptionUnsupportedByPlatformException( + "Unsupported encryption Filter: " + filter + + "; only Standard is supported."); + } + } + } + + /** + * Create a decrypter working from a crypt filter dictionary, as in + * version 4 encryption + * + * @param encryptDict the Encrypt dictionary + * @param documentId the document ID + * @param password the provided password + * @param v the version of encryption being used; must be at least 4 + * @return the decrypter corresponding to the scheme expressed in + * encryptDict + * @throws PDFAuthenticationFailureException if the provided password + * does not decrypt this document + * @throws IOException if there is a problem reading the PDF, an invalid + * document structure, or an inability to obtain the required ciphers + * from the platform's JCE + * @throws EncryptionUnsupportedByPlatformException if the encryption + * is not supported by the environment in which the code is executing + * @throws EncryptionUnsupportedByProductException if PDFRenderer does + * not currently support the specified encryption + */ + private static PDFDecrypter createCryptFilterDecrypter( + PDFObject encryptDict, + PDFObject documentId, + PDFPassword password, + int v) + throws + PDFAuthenticationFailureException, + IOException, + EncryptionUnsupportedByPlatformException, + EncryptionUnsupportedByProductException { + + assert v >= 4 : "crypt filter decrypter not supported for " + + "standard encryption prior to version 4"; + + // encryptMetadata is true if not present. Note that we don't actually + // use this to change our reading of metadata streams (that's all done + // internally by the document specifying a Crypt filter of None if + // appropriate), but it does affect the encryption key. + boolean encryptMetadata = true; + final PDFObject encryptMetadataObj = + encryptDict.getDictRef("EncryptMetadata"); + if (encryptMetadataObj != null + && encryptMetadataObj.getType() == PDFObject.BOOLEAN) { + encryptMetadata = encryptMetadataObj.getBooleanValue(); + } + + final PDFObject bitLengthObj = encryptDict.getDictRef("Length"); + + // Assemble decrypters for each filter in the + // crypt filter (CF) dictionary + final Map cfDecrypters = + new HashMap(); + final PDFObject cfDict = encryptDict.getDictRef("CF"); + if (cfDict == null) { + throw new PDFParseException( + "No CF value present in Encrypt dict for V4 encryption"); + } + final Iterator cfNameIt = cfDict.getDictKeys(); + while (cfNameIt.hasNext()) { + final String cfName = cfNameIt.next(); + final PDFObject cryptFilter = cfDict.getDictRef(cfName); + + final PDFObject lengthObj = cryptFilter.getDictRef("Length"); + // The Errata for PDF 1.7 explains that the value of + // Length in CF dictionaries is in bytes + final Integer length = lengthObj != null ? lengthObj.getIntValue() * 8 : + (bitLengthObj != null) ? bitLengthObj.getIntValue() : null; + + // CFM is the crypt filter method, describing whether RC4, + // AES, or None (i.e., identity) is the encryption mechanism + // used for the name crypt filter + final PDFObject cfmObj = cryptFilter.getDictRef("CFM"); + final String cfm = cfmObj != null ? + cfmObj.getStringValue() : "None"; + final PDFDecrypter cfDecrypter; + if ("None".equals(cfm)) { + cfDecrypter = IdentityDecrypter.getInstance(); + } else if ("V2".equals(cfm)) { + cfDecrypter = createStandardDecrypter( + encryptDict, documentId, password, length, + encryptMetadata, + StandardDecrypter.EncryptionAlgorithm.RC4); + } else if ("AESV2".equals(cfm)) { + cfDecrypter = createStandardDecrypter( + encryptDict, documentId, password, length, + encryptMetadata, + StandardDecrypter.EncryptionAlgorithm.AESV2); + } else { + throw new UnsupportedOperationException( + "Unknown CryptFilter method: " + cfm); + } + cfDecrypters.put(cfName, cfDecrypter); + } + + // always put Identity in last so that it will override any + // Identity filter sneakily declared in the CF entry + cfDecrypters.put(CF_IDENTITY, IdentityDecrypter.getInstance()); + + PDFObject stmFObj = encryptDict.getDictRef("StmF"); + final String defaultStreamFilter = + stmFObj != null ? stmFObj.getStringValue() : CF_IDENTITY; + + PDFObject strFObj = encryptDict.getDictRef("StrF"); + final String defaultStringFilter = + strFObj != null ? strFObj.getStringValue() : CF_IDENTITY; + + return new CryptFilterDecrypter( + cfDecrypters, defaultStreamFilter, defaultStringFilter); + + } + + /** + * Create a standard single-algorithm AES or RC4 decrypter. The Encrypt + * dictionary is used where possible, but where different encryption + * versions employ different mechanisms of specifying configuration or may + * be specified via a CF entry (e.g. key length), the value is specified as + * a parameter. + * + * @param encryptDict the Encrypt dictionary + * @param documentId the document ID + * @param password the password + * @param keyLength the key length, in bits; may be null + * to use a {@link #DEFAULT_KEY_LENGTH default} + * @param encryptMetadata whether metadata is being encrypted + * @param encryptionAlgorithm, the encryption algorithm + * @return the decrypter + * @throws PDFAuthenticationFailureException if the provided password + * is not the one expressed by the encryption dictionary + * @throws IOException if there is a problem reading the PDF content, + * if the content does not comply with the PDF specification + * @throws EncryptionUnsupportedByPlatformException if the encryption + * is not supported by the environment in which the code is executing + * @throws EncryptionUnsupportedByProductException if PDFRenderer does + * not currently support the specified encryption + * + */ + private static PDFDecrypter createStandardDecrypter( + PDFObject encryptDict, + PDFObject documentId, + PDFPassword password, + Integer keyLength, + boolean encryptMetadata, + StandardDecrypter.EncryptionAlgorithm encryptionAlgorithm) + throws + PDFAuthenticationFailureException, + IOException, + EncryptionUnsupportedByPlatformException, + EncryptionUnsupportedByProductException { + + if (keyLength == null) { + keyLength = DEFAULT_KEY_LENGTH; + } + + // R describes the revision of the security handler + final PDFObject rObj = encryptDict.getDictRef("R"); + if (rObj == null) { + throw new PDFParseException( + "No R entry present in Encrypt dictionary"); + } + + final int revision = rObj.getIntValue(); + if (revision < 2 || revision > 4) { + throw new EncryptionUnsupportedByPlatformException( + "Unsupported Standard security handler revision; R=" + + revision); + } + + // O describes validation details for the owner key + final PDFObject oObj = encryptDict.getDictRef("O"); + if (oObj == null) { + throw new PDFParseException( + "No O entry present in Encrypt dictionary"); + } + final byte[] o = oObj.getStream(); + if (o.length != 32) { + throw new PDFParseException("Expected owner key O " + + "value of 32 bytes; found " + o.length); + } + + // U describes validation details for the user key + final PDFObject uObj = encryptDict.getDictRef("U"); + if (uObj == null) { + throw new PDFParseException( + "No U entry present in Encrypt dictionary"); + } + final byte[] u = uObj.getStream(); + if (u.length != 32) { + throw new PDFParseException( + "Expected user key U value of 32 bytes; found " + o.length); + } + + // P describes the permissions regarding document usage + final PDFObject pObj = encryptDict.getDictRef("P"); + if (pObj == null) { + throw new PDFParseException( + "Required P entry in Encrypt dictionary not found"); + } + + return new StandardDecrypter( + encryptionAlgorithm, documentId, keyLength, + revision, o, u, pObj.getIntValue(), encryptMetadata, password); + } + + /** + * @param encryptDict the Encrypt dict as found in the document's trailer. + * @return true if the Filter exist in the current dictionary + */ + public static boolean isFilterExist(PDFObject encryptDict) { + if (encryptDict != null) { + try { + PDFObject filter = encryptDict.getDictRef("Filter"); + return filter != null; + } catch (IOException e) { + } + } + return false; + } + +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/PDFPassword.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/PDFPassword.java new file mode 100644 index 0000000000..9850155a29 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/PDFPassword.java @@ -0,0 +1,279 @@ +/* Copyright 2008 Pirion Systems Pty Ltd, 139 Warry St, + * Fortitude Valley, Queensland, Australia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.decrypt; + +import com.github.librepdf.pdfrenderer.PDFDocCharsetEncoder; +import com.github.librepdf.pdfrenderer.Identity8BitCharsetEncoder; +import com.github.librepdf.pdfrenderer.PDFStringUtil; + +import java.util.*; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CharsetEncoder; +import java.nio.CharBuffer; +import java.nio.ByteBuffer; + +/** + *

    Identifies a PDF Password, expressible either as a string or a + * byte sequence.

    + * + *

    In revisions up to version 1.e Expansion 3, the mapping between a string + * and the bytes corresponding to the password was poorly specified, meaning + * that the safest manner in which to specify a password was via a byte array. + * With 1.7 expansion 3, a still slightly problematic mapping was given for the + * Standard encryption algorithms through to version 4, and a very well + * specified mapping for the new version 5 encryption.

    + * + *

    So, for passwords specified in versions up to and including 4, a byte[] + * representation is the most accurate, but not necessarily the most convenient + * manner to provide passwords. For version 5, allowing passwords to be + * specified as Strings will be the preferred mechanism. Rather than specify two + * interfaces whenever a password can be provided - one for byte[] and one for + * String - we express the password as a class. This class can also offer a best + * guess at a String representation for a password for encryption versions up to + * and including 4.

    + * + * @author Luke Kirby + */ +public class PDFPassword { + + /** The empty password */ + public static final PDFPassword EMPTY_PASSWORD = + new PDFPassword(new byte[0]); + + /** + * Ensure a non-null PDFPassword by substituting the empty password + * for a null password + * @param password the password, may be null + * @return a non-null password + */ + public static PDFPassword nonNullPassword(PDFPassword password) { + return password != null ? password : EMPTY_PASSWORD; + } + + /** the password in bytes, if specified as such */ + private byte[] passwordBytes = null; + /** the passwird as a string, if specified as such */ + private String passwordString = null; + + /** + * Construct a byte-based password + * @param passwordBytes the password bytes + */ + public PDFPassword(byte[] passwordBytes) { + this.passwordBytes = + passwordBytes != null ? passwordBytes : new byte[0]; + } + + /** + * Construct a string-based password + * @param passwordString the password + */ + public PDFPassword(String passwordString) { + this.passwordString = passwordString != null ? passwordString : ""; + } + + /** + * Get the password bytes. + * + * @param unicodeConversion whether the specific conversion from a unicode + * String, as present for version 5 encryption, should be used + * @return a list of possible password bytes + */ + List getPasswordBytes(boolean unicodeConversion) { + // TODO - handle unicodeConversion when we support version 5 + if (this.passwordBytes != null || this.passwordString == null) { + return Collections.singletonList(this.passwordBytes); + } else { + if (isAlphaNum7BitString(this.passwordString)) { + // there's no reasonthat this string would get encoded + // in any other way + return Collections.singletonList( + PDFStringUtil.asBytes(this.passwordString)); + } else { + return generatePossiblePasswordBytes(this.passwordString); + } + } + } + + /** + * An array of password byte generators that attempts to enumerate the + * possible strategies that an encrypting application might take to convert + * a string to an array of bytes + */ + private final static PasswordByteGenerator[] PASSWORD_BYTE_GENERATORS = + new PasswordByteGenerator[]{ + + // The best option, and that recommended by the spec, is + // straight PDFDocEncoding of the string but its not + // mentioned what to do with undefined characters + // (presumably, an encryption generating app should not + // allow them, but there are no guarantees!). Plus, that + // hasn't always been the case. There's also a possiblity + // that we'll be presented with the byte encoding from + // whatever code page is default on the system that + // generated the password. I don't think we're going to try + // all different code pages, though. Here are + // a few ideas, anyway! + + // skip undefined chars + new PDFDocEncodingByteGenerator(null), + // replace undefined chars with 0 + new PDFDocEncodingByteGenerator(Byte.valueOf((byte) 0)), + // replace undefined chars with ? + new PDFDocEncodingByteGenerator(Byte.valueOf((byte) '?')), + // just strip the higher 8 bits! + new PasswordByteGenerator() { + @Override + public byte[] generateBytes(String password) { + return PDFStringUtil.asBytes(password); + } + }, + // skip 2-byte chars + new IdentityEncodingByteGenerator(null), + // replace 2-byte chars with 0 + new IdentityEncodingByteGenerator(Byte.valueOf((byte) 0)), + // replace 2-byte chars with ? + new IdentityEncodingByteGenerator(Byte.valueOf((byte) '?')) + }; + + /** + * Generate some possible byte representations of a string password + * + * @param passwordString the string password + * @return a list of unique possible byte representations + */ + private static List generatePossiblePasswordBytes( + String passwordString) { + + final List possibilties = new ArrayList(); + for (final PasswordByteGenerator generator : PASSWORD_BYTE_GENERATORS) { + byte[] generated = generator.generateBytes(passwordString); + // avoid duplicates + boolean alreadyGenerated = false; + for (int i = 0; !alreadyGenerated && i < possibilties.size(); ++i) { + if (Arrays.equals(possibilties.get(i), generated)) { + alreadyGenerated = true; + } + } + if (!alreadyGenerated) { + possibilties.add(generated); + } + } + return possibilties; + } + + private boolean isAlphaNum7BitString(String string) { + for (int i = 0; i < string.length(); ++i) { + final char c = string.charAt(i); + if (c >= 127 || !Character.isLetterOrDigit(c)) { + return false; + } + } + return true; + } + + /** + * Converts a string password to a byte[] representation + */ + private static interface PasswordByteGenerator { + byte[] generateBytes(String password); + } + + /** + * Converts strings to byte by employing a {@link CharsetEncoder} and a + * configurable mechanism to replace or ignore characters that are + * unrepresentable according to the encoder. + */ + private static abstract class CharsetEncoderGenerator + implements PasswordByteGenerator { + + private Byte replacementByte; + + /** + * Class constructor + * + * @param replacementByte the byte to replace to use to represent any + * unrepresentable character, or null if unrepresentable characters + * should just be ignored + */ + protected CharsetEncoderGenerator(Byte replacementByte) { + this.replacementByte = replacementByte; + } + + + @Override + public byte[] generateBytes(String password) { + final CharsetEncoder encoder = createCharsetEncoder(); + if (this.replacementByte != null) { + encoder.replaceWith(new byte[]{this.replacementByte}); + encoder.onUnmappableCharacter(CodingErrorAction.REPLACE); + } else { + encoder.onUnmappableCharacter(CodingErrorAction.IGNORE); + } + try { + final ByteBuffer b = encoder.encode(CharBuffer.wrap(password)); + final byte[] bytes = new byte[b.remaining()]; + b.get(bytes); + return bytes; + } catch (CharacterCodingException e) { + // shouldn't happen: unmappable characters should be the only + // problem, and we're not handling them with a report + return null; + } + } + + protected abstract CharsetEncoder createCharsetEncoder(); + + } + + /** + * Generate byte[] representations based on the PDFDocEncoding + */ + private static class PDFDocEncodingByteGenerator + extends CharsetEncoderGenerator { + + private PDFDocEncodingByteGenerator(Byte replacementByte) { + super(replacementByte); + } + + @Override + protected CharsetEncoder createCharsetEncoder() { + return new PDFDocCharsetEncoder(); + } + } + + /** + * Generate byte[] representations based on a Unicode code point identity + * encoding; characters over 255 in value are considered unrepresentable + */ + private static class IdentityEncodingByteGenerator + extends CharsetEncoderGenerator { + + private IdentityEncodingByteGenerator(Byte replacementByte) { + super(replacementByte); + } + + @Override + protected CharsetEncoder createCharsetEncoder() { + return new Identity8BitCharsetEncoder(); + } + } + +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/StandardDecrypter.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/StandardDecrypter.java new file mode 100644 index 0000000000..8af610c564 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/StandardDecrypter.java @@ -0,0 +1,1137 @@ +/* Copyright 2008 Pirion Systems Pty Ltd, 139 Warry St, + * Fortitude Valley, Queensland, Australia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.decrypt; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.List; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; +import com.github.librepdf.pdfrenderer.PDFStringUtil; + +/** + * Standard simple decrypter for versions 1, 2 and 4 of the Standard + * password-based decryption mechanisms, as described in section 3.5 of + * the PDF Reference version 1.7. + * + * @author Luke Kirby + */ +public class StandardDecrypter implements PDFDecrypter { + + /** + * Extra salt to add to AES-based decryption keys, as per PDF Reference 1.7 + */ + private static final byte[] AESV2_SALT = {'s', 'A', 'l', 'T'}; + + /** + * Describes an encryption algorithm to be used, declaring not only the + * cipher type, but also key generation techniques + */ + public enum EncryptionAlgorithm { + RC4, AESV2; + + boolean isRC4() { + return this == RC4; + } + + boolean isAES() { + return this == AESV2; + } + + } + + /** + * Padding used to bring passwords up to 32 bytes, as specified by the + * first step of Algorithm 3.2 in the PDF Reference version 1.7. + */ + private final static byte[] PW_PADDING = new byte[]{ + 0x28, (byte) 0xBF, 0x4E, 0x5E, 0x4E, 0x75, (byte) 0x8A, 0x41, + 0x64, 0x00, 0x4E, 0x56, (byte) 0xFF, (byte) 0xFA, 0x01, 0x08, + 0x2E, 0x2E, 0x00, (byte) 0xB6, (byte) 0xD0, 0x68, 0x3E, (byte) 0x80, + 0x2F, 0x0C, (byte) 0xA9, (byte) 0xFE, 0x64, 0x53, 0x69, 0x7A + }; + + /** + * The specification of the RC4 cipher for JCE interactions + */ + private static final String CIPHER_RC4 = "RC4"; + /** + * The key type for RC4 keys + */ + private static final String KEY_RC4 = "RC4"; + + /** + * The specification of the AES cipher for JCE interactions. As per the + * spec, cipher-block chanining (CBC) mode and PKCS5 padding are used + */ + private static final String CIPHER_AES = "AES/CBC/PKCS5Padding"; + /** + * The key type for AES keys + */ + private static final String KEY_AES = "AES"; + + /** + * Whether the owner password was specified + */ + private boolean ownerAuthorised = false; + + /** + * The general encryption key; may be mutated to form individual + * stream/string encryption keys + */ + private byte[] generalKeyBytes; + + /** + * The encryption algorithm being employed + */ + private EncryptionAlgorithm encryptionAlgorithm; + + /** + * Class constructor + * + * @param encryptionAlgorithm the algorithm used for encryption + * @param documentId the contents of the ID entry of the document's trailer + * dictionary; can be null, but according to the spec, shouldn't be. Is + * expected to be an array of two byte sequences. + * @param keyBitLength the length of the key in bits; should be a multiple + * of 8 between 40 and 128 + * @param revision the revision of the Standard encryption security handler + * being employed. Should be 2, 3 or 4. + * @param oValue the value of the O entry from the Encrypt dictionary + * @param uValue the value of the U entry from the Encrypt dictionary + * @param pValue the value of the P entry from the Encrypt dictionary + * @param encryptMetadata whether metadata is being encrypted, as identified + * by the Encrypt dict (with default true if not explicitly identified) + * @param password the password; not null + * @throws IOException if there's a problem reading the file + * @throws EncryptionUnsupportedByPlatformException if the encryption is not + * supported by the environment in which the code is executing + * @throws EncryptionUnsupportedByProductException if PDFRenderer does not + * currently support the specified encryption + */ + public StandardDecrypter( + EncryptionAlgorithm encryptionAlgorithm, + PDFObject documentId, int keyBitLength, + int revision, byte[] oValue, byte[] uValue, int pValue, + boolean encryptMetadata, PDFPassword password) + throws + IOException, + EncryptionUnsupportedByProductException, + EncryptionUnsupportedByPlatformException { + + this.encryptionAlgorithm = encryptionAlgorithm; + + // The spec (sensibly) demands that the documentId be present, + // but we'll play it safe + final byte[] firstDocIdValue; + if (documentId == null) { + firstDocIdValue = null; + } else { + firstDocIdValue = documentId.getAt(0).getStream(); + } + + testJceAvailability(keyBitLength); + + try { + final List passwordBytePossibilities = + password.getPasswordBytes(false); + for (int i = 0; + this.generalKeyBytes == null && i < passwordBytePossibilities.size(); + ++i) { + final byte[] passwordBytes = passwordBytePossibilities.get(i); + this.generalKeyBytes = checkOwnerPassword( + passwordBytes, firstDocIdValue, keyBitLength, + revision, oValue, uValue, pValue, encryptMetadata); + if (this.generalKeyBytes != null) { + // looks like the password was the owner password! + this.ownerAuthorised = true; + } else { + // try it as the user password + this.generalKeyBytes = checkUserPassword( + passwordBytes, firstDocIdValue, keyBitLength, + revision, oValue, uValue, pValue, encryptMetadata); + + } + } + } catch (GeneralSecurityException e) { + // Unexpected, as our test of JCE availability should have caught + // problems with cipher availability. + // It may well be a problem with document content? + throw new PDFParseException("Unable to check passwords: " + + e.getMessage(), e); + } + + if (this.generalKeyBytes == null) { + throw new PDFAuthenticationFailureException( + "Password failed authentication for both " + + "owner and user password"); + } + + } + + @Override + public ByteBuffer decryptBuffer( + String cryptFilterName, PDFObject streamObj, ByteBuffer streamBuf) + throws PDFParseException { + + if (cryptFilterName != null) { + throw new PDFParseException( + "This encryption version does not support Crypt filters"); + } + + if (streamObj != null) { + checkNums(streamObj.getObjNum(), streamObj.getObjGen()); + } + + final byte[] decryptionKeyBytes; + if (streamObj == null) { + // lack of a stream object indicates the unsalted key should be + // used + decryptionKeyBytes = getUnsaltedDecryptionKey(); + } else { + decryptionKeyBytes = getObjectSaltedDecryptionKey( + streamObj.getObjNum(), streamObj.getObjGen()); + } + return decryptBuffer(streamBuf, decryptionKeyBytes); + } + + @Override + public String decryptString(int objNum, int objGen, String inputBasicString) + throws PDFParseException { + final byte[] crypted = PDFStringUtil.asBytes(inputBasicString); + final byte[] decryptionKey = getObjectSaltedDecryptionKey(objNum, objGen); + final ByteBuffer decrypted = decryptBuffer(ByteBuffer.wrap(crypted), decryptionKey); + return PDFStringUtil.asBasicString(decrypted.array(), decrypted.arrayOffset(), decrypted.limit()); + } + + @Override + public boolean isOwnerAuthorised() { + return this.ownerAuthorised; + } + + @Override + public boolean isEncryptionPresent() { + return true; + } + + @Override + public boolean isEncryptionPresent(String cryptFilterName) { + return true; + } + + /** + * Test that the platform (i.e., the JCE) can offer us all of the ciphers at + * the key length we need for content decryption. This shouldn't be a + * problem on the Java 5 platform unless a particularly restrictive policy + * file is in place. Calling this on construction should avoid problems like + * these being exposed as PDFParseExceptions as they're used during + * decryption and key establishment. + * + * @param keyBitLength the length of the content key, in bits + * @throws EncryptionUnsupportedByPlatformException if the platform does not + * support the required ciphers and key lengths + * @throws PDFParseException if there's an internal error while testing + * cipher availability + */ + private void testJceAvailability(int keyBitLength) + throws + EncryptionUnsupportedByPlatformException, PDFParseException { + + // we need to supply a little buffer for AES, which will look + // for an initialisation vector of 16 bytes + final byte[] junkBuffer = new byte[16]; + Arrays.fill(junkBuffer, (byte) 0xAE); + // test using the longer key length for salted content so that + // we can check for maximum key length problems + final byte[] junkKey = + new byte[getSaltedContentKeyByteLength(keyBitLength / 8)]; + Arrays.fill(junkKey, (byte) 0xAE); + + try { + createAndInitialiseContentCipher( + ByteBuffer.wrap(junkBuffer), + junkKey); + } catch (PDFParseException e) { + throw new PDFParseException("Internal error; " + + "failed to produce test cipher: " + e.getMessage()); + } catch (NoSuchAlgorithmException e) { + throw new EncryptionUnsupportedByPlatformException( + "JCE does not offer required cipher", e); + } catch (NoSuchPaddingException e) { + throw new EncryptionUnsupportedByPlatformException( + "JCE does not offer required padding", e); + } catch (InvalidKeyException e) { + throw new EncryptionUnsupportedByPlatformException( + "JCE does accept key size of " + + (getSaltedContentKeyByteLength() * 8) + + " bits- could it be a policy restriction?", e); + } catch (InvalidAlgorithmParameterException e) { + throw new EncryptionUnsupportedByPlatformException( + "JCE did not accept cipher parameter", e); + } + + try { + createMD5Digest(); + } catch (NoSuchAlgorithmException e) { + throw new EncryptionUnsupportedByPlatformException( + "No MD5 digest available from JCE", e); + } + + if (this.encryptionAlgorithm != EncryptionAlgorithm.RC4) { + // we still need RC4 for U and O value checks. Check again! + final Cipher rc4; + try { + rc4 = createRC4Cipher(); + } catch (GeneralSecurityException e) { + throw new EncryptionUnsupportedByPlatformException( + "JCE did not offer RC4 cipher", e); + } + // 40 byte key is used for base U and O ciphers + final byte[] rc4JunkKey = new byte[5]; + Arrays.fill(junkKey, (byte) 0xAE); + try { + initDecryption(rc4, createRC4Key(rc4JunkKey)); + } catch (InvalidKeyException ex) { + throw new EncryptionUnsupportedByPlatformException( + "JCE did not accept 40-bit RC4 key; " + + "policy problem?", + ex); + } + } + } + + /** + * Decrypt a buffer + * + * @param encrypted the encrypted content + * @param decryptionKeyBytes the key to use for decryption + * @return a freshly allocated buffer containing the decrypted content + * @throws PDFParseException if there's a problem decrypting the content + */ + private ByteBuffer decryptBuffer( + ByteBuffer encrypted, byte[] decryptionKeyBytes) + throws PDFParseException { + + final Cipher cipher; + try { + cipher = createAndInitialiseContentCipher( + encrypted, decryptionKeyBytes); + } catch (GeneralSecurityException e) { + // we should have caught this earlier in testCipherAvailability + throw new PDFParseException( + "Unable to create cipher due to platform limitation: " + + e.getMessage(), e); + } + + try { + // the decrypted content will never be more than the encrypted + // content. Thanks to padding, this buffer will be at most 16 + // bytes bigger than the encrypted content + final ByteBuffer decryptedBuf = + ByteBuffer.allocate(encrypted.remaining()); + cipher.doFinal(encrypted, decryptedBuf); + decryptedBuf.flip(); + return decryptedBuf; + } catch (GeneralSecurityException e) { + throw new PDFParseException( + "Could not decrypt: " + e.getMessage(), e); + } + } + + /** + * Setup the cipher for decryption + * + * @param encrypted the encrypted content; required by AES encryption so + * that the initialisation vector can be established + * @param decryptionKeyBytes the bytes for the decryption key + * @return a content decryption cypher, ready to accept input + * @throws PDFParseException if the encrypted buffer is malformed or on an + * internal error + * @throws NoSuchAlgorithmException if the cipher algorithm is not supported + * by the platform + * @throws NoSuchPaddingException if the cipher padding is not supported by + * the platform + * @throws InvalidKeyException if the key is invalid according to the + * cipher, or too long + * @throws InvalidAlgorithmParameterException if the cipher parameters are + * bad + */ + private Cipher createAndInitialiseContentCipher( + ByteBuffer encrypted, + byte[] decryptionKeyBytes) + throws + PDFParseException, + NoSuchAlgorithmException, + NoSuchPaddingException, + InvalidKeyException, + InvalidAlgorithmParameterException { + + final Cipher cipher; + if (this.encryptionAlgorithm.isRC4()) { + cipher = Cipher.getInstance(CIPHER_RC4); + cipher.init(Cipher.DECRYPT_MODE, createRC4Key(decryptionKeyBytes)); + } else if (this.encryptionAlgorithm.isAES()) { + cipher = createAESCipher(); + final byte[] initialisationVector = new byte[16]; + if (encrypted.remaining() >= initialisationVector.length) { + encrypted.get(initialisationVector); + } else { + throw new PDFParseException( + "AES encrypted stream too short - " + + "no room for initialisation vector"); + } + + final SecretKeySpec aesKey = + new SecretKeySpec(decryptionKeyBytes, KEY_AES); + final IvParameterSpec aesIv = + new IvParameterSpec(initialisationVector); + cipher.init(Cipher.DECRYPT_MODE, aesKey, aesIv); + } else { + throw new PDFParseException( + "Internal error - unhandled cipher type: " + + this.encryptionAlgorithm); + } + return cipher; + } + + /** + * Get the unsalted content decryption key, used for streams with specific + * crypt filters, which aren't specific to particular objects + * + * @return the general key + */ + private byte[] getUnsaltedDecryptionKey() { + return this.generalKeyBytes; + } + + /** + * Get a decryption key salted with an object number and object generation, + * for use when decrypting a string or stream within an object numbered so + * + * @param objNum the object number + * @param objGen the object generation + * @return the key to be used for decrypting data associated with the object + * numbered so + * @throws PDFParseException if the MD5 digest is not available + */ + private byte[] getObjectSaltedDecryptionKey(int objNum, int objGen) + throws PDFParseException { + + byte[] decryptionKeyBytes; + final MessageDigest md5; + try { + md5 = createMD5Digest(); + } catch (NoSuchAlgorithmException e) { + // unexpected, as we will already have tested availability + throw new PDFParseException("Unable to get MD5 digester", e); + } + md5.update(this.generalKeyBytes); + md5.update((byte) objNum); + md5.update((byte) (objNum >> 8)); + md5.update((byte) (objNum >> 16)); + md5.update((byte) objGen); + md5.update((byte) (objGen >> 8)); + if (this.encryptionAlgorithm == EncryptionAlgorithm.AESV2) { + md5.update(AESV2_SALT); + } + final byte[] hash = md5.digest(); + final int keyLen = getSaltedContentKeyByteLength(); + decryptionKeyBytes = new byte[keyLen]; + System.arraycopy(hash, 0, decryptionKeyBytes, 0, keyLen); + return decryptionKeyBytes; + } + + /** + * Get the length of a salted key + * + * @return length in bytes + */ + private int getSaltedContentKeyByteLength() { + return getSaltedContentKeyByteLength(this.generalKeyBytes.length); + } + + /** + * Get the length of salted keys, in bytes. Unsalted keys will be the same + * length as {@link #generalKeyBytes} + * + * @param generalKeyByteLength the length of the general key, in bytes + * @return byte length of salted keys + */ + private int getSaltedContentKeyByteLength(int generalKeyByteLength) { + return Math.min(generalKeyByteLength + 5, 16); + } + + /** + * Check that object number and object generations are well-formed. It is + * possible for some {@link PDFObject}s to have uninitialised object numbers + * and generations, but such objects should not required decryption + * + * @param objNum the object number + * @param objGen the object generation + * @throws PDFParseException if the object numbering indicates that they + * aren't true object numbers + */ + private void checkNums(int objNum, int objGen) + throws PDFParseException { + if (objNum < 0) { + throw new PDFParseException( + "Internal error: Object has bogus object number"); + } else if (objGen < 0) { + throw new PDFParseException( + "Internal error: Object has bogus generation number"); + } + } + + /** + * Calculate what the U value should consist of given a particular key and + * document configuration. Correponds to Algorithms 3.4 and 3.5 of the + * PDF Reference version 1.7 + * + * @param generalKey the general encryption key + * @param firstDocIdValue the value of the first element in the document's + * ID entry in the trailer dictionary + * @param revision the revision of the security handler + * @return the U value for the given configuration + * @throws GeneralSecurityException if there's an error getting required + * ciphers, etc. (unexpected, since a check for algorithm availability is + * performed on construction) + * @throws EncryptionUnsupportedByProductException if the revision is not + * supported + */ + private byte[] calculateUValue( + byte[] generalKey, byte[] firstDocIdValue, int revision) + throws + GeneralSecurityException, + EncryptionUnsupportedByProductException { + + if (revision == 2) { + + // Algorithm 3.4: Computing the encryption dictionary’s U (user + // password) value (Revision 2) + + // Step 1 is provided to us as the parameter generalKey: + // Create an encryption key based on the user password string, as + // described in Algorithm 3.2 + + // Step 2: Encrypt the 32-byte padding string shown in step 1 of + // Algorithm 3.2, using an RC4 encryption function with the + // encryption key from the preceding step. + + Cipher rc4 = createRC4Cipher(); + SecretKey key = createRC4Key(generalKey); + initEncryption(rc4, key); + return crypt(rc4, PW_PADDING); + + } else if (revision >= 3) { + + // Algorithm 3.5: Computing the encryption dictionary’s U (user + // password) value (Revision 3 or greater) + + // Step 1 is provided to us as the parameter generalKey: + // Create an encryption key based on the user password string, as + // described in Algorithm 3.2 + + // Step 2: Initialize the MD5 hash function and pass the 32-byte + // padding string shown in step 1 of Algorithm 3.2 as input to this + // function + MessageDigest md5 = createMD5Digest(); + md5.update(PW_PADDING); + + // Step 3: Pass the first element of the file’s file identifier + // array (the value of the ID entry in the document’s trailer + // dictionary; see Table 3.13 on page 97) to the hash function and + // finish the hash. (See implementation note 26 in Appendix H.) + if (firstDocIdValue != null) { + md5.update(firstDocIdValue); + } + final byte[] hash = md5.digest(); + + // Step 4: Encrypt the 16-byte result of the hash, using an RC4 + // encryption function with the encryption key from step 1. + Cipher rc4 = createRC4Cipher(); + SecretKey key = createRC4Key(generalKey); + initEncryption(rc4, key); + final byte[] v = crypt(rc4, hash); + + // Step 5: Do the following 19 times: Take the output from the + // previous invocation of the RC4 function and pass it as input to + // a new invocation of the function; use an encryption key generated + // by taking each byte of the original encryption key (obtained in + // step 1) and performing an XOR (exclusive or) operation between + // that byte and the single-byte value of the iteration counter + // (from 1 to 19). + rc4shuffle(v, generalKey, rc4); + + // Step 6: Append 16 bytes of arbitrary padding to the output from + // the final invocation of the RC4 function and store the 32-byte + // result as the value of the U entry in the encryption dictionary. + assert v.length == 16; + final byte[] entryValue = new byte[32]; + System.arraycopy(v, 0, entryValue, 0, v.length); + System.arraycopy(v, 0, entryValue, 16, v.length); + return entryValue; + + } else { + throw new EncryptionUnsupportedByProductException( + "Unsupported standard security handler revision " + + revision); + } + } + + /** + * Calculate what the O value of the Encrypt dict should look like given a + * particular configuration. Not used, but useful for reference; this + * process is reversed to determine whether a given password is the + * owner password. Corresponds to Algorithm 3.3 of the PDF Reference + * version 1.7. + * + * @see #checkOwnerPassword + * @param ownerPassword the owner password + * @param userPassword the user password + * @param keyBitLength the key length in bits (40-128) + * @param revision the security handler revision + * @return the O value entry + * @throws GeneralSecurityException if ciphers are unavailable or + * inappropriately used + */ + private byte[] calculuateOValue( + byte[] ownerPassword, byte[] userPassword, + int keyBitLength, int revision) + throws GeneralSecurityException { + + // Steps 1-4 + final byte[] rc4KeyBytes = + getInitialOwnerPasswordKeyBytes( + ownerPassword, keyBitLength, revision); + final Cipher rc4 = createRC4Cipher(); + initEncryption(rc4, createRC4Key(rc4KeyBytes)); + + // Step 5: Pad or truncate the user password string as described in step + // 1 of Algorithm 3.2. + // Step 6: Encrypt the result of step 5, using an RC4 encryption + // function with the encryption key obtained in step 4. + byte[] pwvalue = crypt(rc4, padPassword(userPassword)); + + // Step 7: (Revision 3 or greater) Do the following 19 times: Take the + // output from the previous invocation of the RC4 function and pass it + // as input to a new invocation of the function; use an encryption key + // generated by taking each byte of the encryption key obtained in step + // 4 and performing an XOR (exclusive or) operation between + if (revision >= 3) { + rc4shuffle(pwvalue, rc4KeyBytes, rc4); + } + assert pwvalue.length == 32; + return pwvalue; + + } + + /** + * Check to see whether a given password is the owner password. Corresponds + * to algorithm 3.6 of PDF Reference version 1.7. + * + * @param ownerPassword the suggested owner password (may be null or + * empty) + * @param firstDocIdValue the byte stream from the first element of the + * value of the ID entry in the trailer dictionary + * @param keyBitLength the key length in bits + * @param revision the security handler revision + * @param oValue the O value from the Encrypt dictionary + * @param uValue the U value from the Encrypt dictionary + * @param pValue the P value from the Encrypt dictionary + * @param encryptMetadata the EncryptMetadata entry from the Encrypt dictionary + * (or false if not present or revision <= 3) + * @return the general/user key bytes if the owner password is currect, + * null otherwise + * @throws GeneralSecurityException if there's a problem with + * cipher or digest usage; unexpected + * @throws EncryptionUnsupportedByProductException if PDFRenderer doesn't + * support the security handler revision + * @throws PDFParseException if the document is malformed + */ + private byte[] checkOwnerPassword( + byte[] ownerPassword, byte[] firstDocIdValue, int keyBitLength, + int revision, byte[] oValue, byte[] uValue, int pValue, + boolean encryptMetadata) + throws + GeneralSecurityException, + EncryptionUnsupportedByProductException, + PDFParseException { + + // Step 1: Compute an encryption key from the supplied password string, + // as described in steps 1 to 4 of Algorithm 3.3. + final byte[] rc4KeyBytes = + getInitialOwnerPasswordKeyBytes(ownerPassword, + keyBitLength, revision); + final Cipher rc4 = createRC4Cipher(); + initDecryption(rc4, createRC4Key(rc4KeyBytes)); + + // Step 2: + final byte[] possibleUserPassword; + if (revision == 2) { + // (Revision 2 only) Decrypt the value of the encryption + // dictionary’s O entry, using an RC4 encryption function with the + // encryption key computed in step 1. + + possibleUserPassword = crypt(rc4, oValue); + } else if (revision >= 3) { + // (Revision 3 or greater) Do the following 20 times: Decrypt the + // value of the encryption dictionary’s O entry (first iteration) or + // the output from the previous iteration (all subsequent + // iterations), using an RC4 encryption function with a different + // encryption key at each iteration. The key is generated by taking + // the original key (obtained in step 1) and performing an XOR + // (exclusive or) operation between each byte of the key and the + // single-byte value of the iteration counter (from 19 to 0). + + // unshuffle the O entry; the unshuffle operation also + // contains the final decryption with the original key + possibleUserPassword = new byte[32]; + System.arraycopy(oValue, 0, possibleUserPassword, 0, + possibleUserPassword.length); + rc4unshuffle(rc4, possibleUserPassword, rc4KeyBytes); + } else { + throw new EncryptionUnsupportedByProductException( + "Unsupported revision: " + revision); + } + + // Step 3: The result of step 2 purports to be the user password. + // Authenticate this user password using Algorithm 3.6. If it is + // correct, the password supplied is the correct owner password. + return checkUserPassword( + possibleUserPassword, firstDocIdValue, keyBitLength, + revision, oValue, uValue, pValue, encryptMetadata); + + } + + /** + * Establish the key to be used for the generation and validation + * of the user password via the O entry. Corresponds to steps 1-4 in + * Algorithm 3.3 of the PDF Reference version 1.7. + * @param ownerPassword the owner password + * @param keyBitLength the length of the key in bits + * @param revision the security handler revision + * @return the key bytes to use for generation/validation of the O entry + * @throws GeneralSecurityException if there's a problem wranling ciphers + */ + private byte[] getInitialOwnerPasswordKeyBytes( + byte[] ownerPassword, int keyBitLength, int revision) + throws GeneralSecurityException { + + final MessageDigest md5 = createMD5Digest(); + + // Step 1: Pad or truncate the owner password string as described in + // step 1 of Algorithm 3.2. If there is no owner password, use the user + // password instead. (See implementation note 27 in Appendix H.) + // Step 2: Initialize the MD5 hash function and pass the result of step 1 as + // input to this function. + md5.update(padPassword(ownerPassword)); + + // Step 3.(Revision 3 or greater) Do the following 50 times: Take the + // output from the previous MD5 hash and pass it as input into a new MD5 + // hash + final byte[] hash = md5.digest(); + if (revision >= 3) { + for (int i = 0; i < 50; ++i) { + md5.update(hash); + digestTo(md5, hash); + } + } + + // Step 4: Create an RC4 encryption key using the first n bytes of + // the output from the final MD5 hash, where n is always 5 for revision + // 2 but, for revision 3 or greater, depends on the value of the + // encryption dictionary’s Length entry + final byte[] rc4KeyBytes = new byte[keyBitLength / 8]; + System.arraycopy(hash, 0, rc4KeyBytes, 0, rc4KeyBytes.length); + return rc4KeyBytes; + } + + /** + * Check to see whether a provided user password is correct with respect + * to an Encrypt dict configuration. Corresponds to algorithm 3.6 of + * the PDF Reference version 1.7 + * @param userPassword the user password to test; may be null or empty + * @param firstDocIdValue the byte stream from the first element of the + * value of the ID entry in the trailer dictionary + * @param keyBitLength the length of the key in bits + * @param revision the security handler revision + * @param oValue the O value from the Encrypt dictionary + * @param uValue the U value from the Encrypt dictionary + * @param pValue the P value from the Encrypt dictionary + * @param encryptMetadata the EncryptMetadata entry from the Encrypt dictionary + * (or false if not present or revision <= 3) + * @return the general/user encryption key if the user password is correct, + * or null if incorrect + * @throws GeneralSecurityException if there's a problem with + * cipher or digest usage; unexpected + * @throws EncryptionUnsupportedByProductException if PDFRenderer doesn't + * support the security handler revision + * @throws PDFParseException if the document is improperly constructed + */ + private byte[] checkUserPassword( + byte[] userPassword, byte[] firstDocIdValue, int keyBitLength, + int revision, byte[] oValue, byte[] uValue, int pValue, + boolean encryptMetadata) + throws + GeneralSecurityException, + EncryptionUnsupportedByProductException, + PDFParseException { + + // Algorithm 3.6: Authenticating the user password + + // Step 1: Perform all but the last step of Algorithm 3.4 (Revision 2) + // or Algorithm 3.5 (Revision 3 or greater) using the supplied password + // string + // + // I.e., figure out what the general key would be with the + // given password + // Algorithm 3.4/5,Step1: + // Determine general key based on user password, as per Algorithm 3.2 + final byte[] generalKey = calculateGeneralEncryptionKey( + userPassword, firstDocIdValue, keyBitLength, + revision, oValue, pValue, encryptMetadata); + // Algorithm 3.4/5,RemainingSteps: + final byte[] calculatedUValue = + calculateUValue(generalKey, firstDocIdValue, revision); + + // Step 2: If the result of step 1 is equal to the value of the + // encryption dictionary’s U entry (comparing on the first 16 bytes in + // the case of Revision 3 or greater), the password supplied is the + // correct user password. The key obtained in step 1 (that is, in the + // first step of Algorithm 3.4 or 3.5) can be used to decrypt the + // document using Algorithm 3.1 on page 119. + assert calculatedUValue.length == 32; + if (uValue.length != calculatedUValue.length) { + throw new PDFParseException("Improper U entry length; " + + "expected 32, is " + uValue.length); + } + // Only the first 16 bytes are significant if using revision > 2 + final int numSignificantBytes = revision == 2 ? 32 : 16; + for (int i = 0; i < numSignificantBytes; ++i) { + if (uValue[i] != calculatedUValue[i]) { + return null; + } + } + return generalKey; + } + + + /** + * Determine what the general encryption key is, given a configuration. This + * corresponds to Algorithm 3.2 of PDF Reference version 1.7. + * + * @param userPassword the desired user password; may be null or empty + * @param firstDocIdValue the byte stream from the first element of the + * value of the ID entry in the trailer dictionary + * @param keyBitLength the length of the key in bits + * @param revision the security handler revision + * @param oValue the O value from the Encrypt dictionary + * @param pValue the P value from the Encrypt dictionary + * @param encryptMetadata the EncryptMetadata entry from the Encrypt + * dictionary (or false if not present or revision <= 3) + * @return the general encryption key + * @throws GeneralSecurityException if an error occurs when obtaining + * and operating ciphers/digests + */ + private byte[] calculateGeneralEncryptionKey( + byte[] userPassword, byte[] firstDocIdValue, int keyBitLength, + int revision, byte[] oValue, int pValue, boolean encryptMetadata) + throws GeneralSecurityException { + + // Algorithm 3.2: Computing an encryption key + + // Step 1: Pad or truncate the password string to exactly 32 bytes... + final byte[] paddedPassword = padPassword(userPassword); + + // Step 2: Initialize the MD5 hash function and pass the result of step + // 1 as input to this function. + MessageDigest md5 = createMD5Digest(); + md5.reset(); + md5.update(paddedPassword); + + // Step 3: Pass the value of the encryption dictionary’s O entry to the + // MD5 hash function. (Algorithm 3.3 shows how the O value is computed.) + md5.update(oValue); + + // Step 4: Treat the value of the P entry as an unsigned 4-byte integer + // and pass these bytes to the MD5 hash function, low-order byte first + md5.update((byte) (pValue & 0xFF)); + md5.update((byte) ((pValue >> 8) & 0xFF)); + md5.update((byte) ((pValue >> 16) & 0xFF)); + md5.update((byte) (pValue >> 24)); + + // Step 5: Pass the first element of the file’s file identifier array + // (the value of the ID entry in the document’s trailer dictionary; see + // Table 3.13 on page 97) to the MD5 hash function. (See implementation + // note 26 in Appendix H.) + if (firstDocIdValue != null) { + md5.update(firstDocIdValue); + } + + // Step 6: (Revision 4 or greater) If document metadata is not being + // encrypted, pass 4 bytes with the value 0xFFFFFFFF to the MD5 hash + // function + if (revision >= 4 && !encryptMetadata) { + for (int i = 0; i < 4; ++i) { + md5.update((byte) 0xFF); + } + } + + // Step 7: finish the hash + byte[] hash = md5.digest(); + + final int keyLen = revision == 2 ? 5 : (keyBitLength / 8); + final byte[] key = new byte[keyLen]; + + // Step 8: (Revision 3 or greater) Do the following 50 times: Take the + // output from the previous MD5 hash and pass the first n bytes of the + // output as input into a new MD5 hash, where n is the number of bytes + // of the encryption key as defined by the value of the encryption + // dictionary’s Length entry + if (revision >= 3) { + for (int i = 0; i < 50; ++i) { + md5.update(hash, 0, key.length); + digestTo(md5, hash); + } + } + + // Set the encryption key to the first n bytes of the output from the + // final MD5 hash, where n is always 5 for revision 2 but, for revision + // 3 or greater, depends on the value of the encryption dictionary’s + // Length entry. + System.arraycopy(hash, 0, key, 0, key.length); + return key; + } + + /** + * Pad a password as per step 1 of Algorithm 3.2 of the PDF Reference + * version 1.7 + * @param password the password, may be null or empty + * @return the padded password, always 32 bytes long + */ + private byte[] padPassword(byte[] password) { + + if (password == null) { + password = new byte[0]; + } + + // Step 1: Pad or truncate the password string to exactly 32 bytes. If + // the password string is more than 32 bytes long, use only its first 32 + // bytes; if it is less than 32 bytes long, pad it by appending the + // required number of additional bytes from the beginning of the + // following padding string: + // < 28 BF 4E 5E 4E 75 8A 41 64 00 4E 56 FF FA 01 08 + // 2E 2E 00 B6 D0 68 3E 80 2F 0C A9 FE 64 53 69 7A > + // That is, if the password string is n bytes long, append the first 32 + // − n bytes of the padding string to the end of the password string. If + // the password string is empty (zero-length), meaning there is no user + // password, substitute the entire padding string in its place. + + byte[] padded = new byte[32]; + // limit password to 32 bytes + final int numContributingPasswordBytes = + password.length > padded.length ? + padded.length : password.length; + System.arraycopy(password, 0, padded, 0, numContributingPasswordBytes); + // Copy padding + if (password.length < padded.length) { + System.arraycopy(PW_PADDING, 0, padded, password.length, + padded.length - password.length); + } + return padded; + } + + /** + * Encrypt some bytes + * + * @param cipher the cipher + * @param input the plaintext + * @return the crypt text + * @throws BadPaddingException if there's bad padding + * @throws IllegalBlockSizeException if the block size is bad + */ + private byte[] crypt(Cipher cipher, byte[] input) + throws IllegalBlockSizeException, BadPaddingException { + return cipher.doFinal(input); + } + + /** + * Initialise a cipher for encryption + * + * @param cipher the cipher + * @param key the encryption key + * @throws InvalidKeyException if the key is invalid for the cipher + */ + private void initEncryption(Cipher cipher, SecretKey key) + throws InvalidKeyException { + cipher.init(Cipher.ENCRYPT_MODE, key); + } + + /** + * Shuffle some input using a series of RC4 encryptions with slight + * mutations of an given key per iteration. Shuffling happens in place. + * Refer to the documentation of the algorithm steps where this is called. + * + * @param shuffle the bytes to be shuffled + * @param key the original key + * @param rc4 the cipher to use + * @throws GeneralSecurityException if there's a problem with cipher + * operation + */ + private void rc4shuffle(byte[] shuffle, byte[] key, Cipher rc4) + throws GeneralSecurityException { + + final byte[] shuffleKey = new byte[key.length]; + for (int i = 1; i <= 19; ++i) { + for (int j = 0; j < shuffleKey.length; ++j) { + shuffleKey[j] = (byte) (key[j] ^ i); + } + initEncryption(rc4, createRC4Key(shuffleKey)); + cryptInPlace(rc4, shuffle); + } + } + + /** + * Reverse the {@link #rc4shuffle} operation, and the operation + * that invariable preceeds it, thereby obtaining an original message + * @param rc4 the RC4 cipher to use + * @param shuffle the bytes in which shuffling will take place; unshuffling + * happens in place + * @param key the encryption key + * @throws GeneralSecurityException if there's a problem with cipher + * operation + */ + private void rc4unshuffle(Cipher rc4, byte[] shuffle, byte[] key) + throws GeneralSecurityException { + + // there's an extra unshuffle at the end with the original key - + // this is why we end with i == 0, where the shuffle key will be the key + final byte[] shuffleKeyBytes = new byte[key.length]; + for (int i = 19; i >= 0; --i) { + for (int j = 0; j < shuffleKeyBytes.length; ++j) { + shuffleKeyBytes[j] = (byte) (key[j] ^ i); + } + initDecryption(rc4, createRC4Key(shuffleKeyBytes)); + cryptInPlace(rc4, shuffle); + } + } + + /** + * Encrypt/decrypt something in place + * @param rc4 the cipher to use; must be a stream cipher producing + * identical output length to input (e.g., RC4) + * @param buffer the buffer to read input from and write output to + * @throws IllegalBlockSizeException if an inappropriate cipher is used + * @throws ShortBufferException if an inappropriate cipher is used + * @throws BadPaddingException if an inappropriate cipher is used + */ + private void cryptInPlace(Cipher rc4, byte[] buffer) + throws IllegalBlockSizeException, ShortBufferException, BadPaddingException { + rc4.doFinal(buffer, 0, buffer.length, buffer); + } + + /** + * Setup a cipher for decryption + * @param cipher the cipher + * @param aKey the cipher key + * @throws InvalidKeyException if the key is of an unacceptable size or + * doesn't belong to the cipher + */ + private void initDecryption(Cipher cipher, Key aKey) + throws InvalidKeyException { + cipher.init(Cipher.DECRYPT_MODE, aKey); + } + + /** + * Create a new RC4 cipher. Should always be available for supported + * platforms. + * @return the cipher + * @throws NoSuchAlgorithmException if the RC4 cipher is unavailable + * @throws NoSuchPaddingException should not happen, as no padding + * is specified + */ + private Cipher createRC4Cipher() + throws NoSuchAlgorithmException, NoSuchPaddingException { + return Cipher.getInstance(CIPHER_RC4); + } + + /** + * Create a new AES cipher. Should always be available for supported + * platforms. + * @return the new cipher + * @throws NoSuchAlgorithmException if the AES cipher is unavailable + * @throws NoSuchPaddingException if the required padding is unavailable + */ + private Cipher createAESCipher() + throws NoSuchAlgorithmException, NoSuchPaddingException { + return Cipher.getInstance(CIPHER_AES); + } + + /** + * Create an MD5 digest. Should always be available for supported + * platforms. + * @return the MD5 digest + * @throws NoSuchAlgorithmException if the digest is not available + */ + private MessageDigest createMD5Digest() + throws NoSuchAlgorithmException { + return MessageDigest.getInstance("MD5"); + } + + /** + * Create an RC4 key + * + * @param keyBytes the bytes for the key + * @return the key + */ + private SecretKeySpec createRC4Key(byte[] keyBytes) { + return new SecretKeySpec(keyBytes, KEY_RC4); + } + + /** + * Hash into an existing byte array + * @param md5 the MD5 digest + * @param hash the hash destination + * @throws GeneralSecurityException if there's a problem hashing; e.g., + * if the buffer is too small + */ + private void digestTo(MessageDigest md5, byte[] hash) + throws GeneralSecurityException { + md5.digest(hash, 0, hash.length); + } + + +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/UnsupportedEncryptionException.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/UnsupportedEncryptionException.java new file mode 100644 index 0000000000..6a00233b57 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/decrypt/UnsupportedEncryptionException.java @@ -0,0 +1,39 @@ +/* + * Copyright 2008 Pirion Systems Pty Ltd, 139 Warry St, + * Fortitude Valley, Queensland, Australia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.decrypt; + +/** + * Identifies that the specified encryption mechanism is not + * supported by this product or platform. + * + * @see EncryptionUnsupportedByPlatformException + * @see EncryptionUnsupportedByProductException + * @author Luke Kirby + */ +public abstract class UnsupportedEncryptionException extends Exception { + + protected UnsupportedEncryptionException(String message) { + super(message); + } + + protected UnsupportedEncryptionException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/BuiltinFont.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/BuiltinFont.java new file mode 100644 index 0000000000..18ee364ef8 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/BuiltinFont.java @@ -0,0 +1,234 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.font; + +import java.awt.Font; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.Properties; + +import com.github.librepdf.pdfrenderer.PDFObject; + +/** + * This class represents the 14 built-in fonts. It reads these fonts + * from files in the "res" directory, as specified in + * BaseNames.properties. + */ +public class BuiltinFont extends Type1Font { + + /** the properties file */ + private static Properties props; + /** the fonts themselves */ + private static Map fonts; + /** the names of the 14 base fonts */ + private static final String[] baseFonts = { + "Courier", "Courier-Bold", "Courier-BoldOblique", "Courier-Oblique", + "Helvetica", "Helvetica-Bold", "Helvetica-BoldOblique", + "Helvetica-Oblique", "Times-Roman", "Times-Bold", "Times-BoldItalic", + "Times-Italic", "Symbol", "ZapfDingbats" + }; + /** fonts others (e.g. Acrobad PDFWriter 3.02 for Windows) assume + * are there, even though they're not in the spec. Grrr... + * + * the format is + */ + private static final String[] mappedFonts = { + // map arial to helvetica + "Arial", "Helvetica", + "Arial,Bold", "Helvetica-Bold", + "Arial,BoldItalic", "Helvetica-BoldOblique", + "Arial,Italic", "Helvetica-Oblique", + // map ArialMT to Helvetica + "ArialMT", "Helvetica", + "Arial-BoldMT", "Helvetica-Bold", + "Arial-BoldItalicMT", "Helvetica-BoldOblique", + "Arial-ItalicMT", "Helvetica-Oblique", + // map TimesNewRoman to Times + "TimesNewRoman", "Times-Roman", + "TimesNewRoman,Bold", "Times-Bold", + "TimesNewRoman,BoldItalic", "Times-BoldItalic", + "TimesNewRoman,Italic", "Times-Italic", + // map TimesNewRomanPSMT to Times + "TimesNewRomanPSMT", "Times-Roman", + "TimesNewRomanPS-BoldMT", "Times-Bold", + "TimesNewRomanPS-BoldItalicMT", "Times-BoldItalic", + "TimesNewRomanPS-ItalicMT", "Times-Italic", + //Map some variants of Courier + "Courier,Bold", "Courier-Bold", + "Courier,BoldItalic", "Courier-BoldOblique", + "Courier,Italic", "Courier-Oblique", + // map CourierNew to Courier + "CourierNew", "Courier", + "CourierNew,Bold", "Courier-Bold", + "CourierNew,BoldItalic", "Courier-BoldOblique", + "CourierNew,Italic", "Courier-Oblique", + }; + + /** + * Create a new Builtin object based on the name of a built-in font + * + * This must be the name of one of the 14 built-in fonts! + * + * @param baseFont the name of the font, from the PDF file + * @param fontObj the object containing font information + */ + public BuiltinFont(String baseFont, PDFObject fontObj) throws IOException { + super(baseFont, fontObj, null); + + parseFont(baseFont); + } + + /** + * create a new BuiltingFont object based on a description of the + * font from the PDF file. Parse the description for key information + * and use that to generate an appropriate font. + */ + public BuiltinFont(String baseFont, PDFObject fontObj, + PDFFontDescriptor descriptor) + throws IOException { + super(baseFont, fontObj, descriptor); + + String fontName = descriptor.getFontName(); + + // check if it's one of the 14 base fonts + for (int i = 0; i < baseFonts.length; i++) { + if (fontName.equalsIgnoreCase(baseFonts[i])) { + parseFont(fontName); + return; + } + } + + // check if it's a mapped font + for (int i = 0; i < mappedFonts.length; i += 2) { + if (fontName.equalsIgnoreCase(mappedFonts[i])) { + parseFont(mappedFonts[i + 1]); + return; + } + } + + int flags = descriptor.getFlags(); + int style = ((flags & PDFFontDescriptor.FORCEBOLD) != 0) ? Font.BOLD : Font.PLAIN; + + if (fontName.indexOf("Bold") > 0) { + style |= Font.BOLD; + } + if ((descriptor.getItalicAngle() != 0) || + ((flags & (PDFFontDescriptor.SCRIPT | PDFFontDescriptor.ITALIC)) != 0)) { + style |= Font.ITALIC; + } + String name = null; + + if ((flags & PDFFontDescriptor.FIXED_PITCH) != 0) { // fixed width + if (((style & Font.BOLD) > 0) && ((style & Font.ITALIC) > 0)) { + name = "Courier-BoldOblique"; + } else if ((style & Font.BOLD) > 0) { + name = "Courier-Bold"; + } else if ((style & Font.ITALIC) > 0) { + name = "Courier-Oblique"; + } else { + name = "Courier"; + } + } else if ((flags & PDFFontDescriptor.SERIF) != 0) { // serif font + if (((style & Font.BOLD) > 0) && ((style & Font.ITALIC) > 0)) { + name = "Times-BoldItalic"; + } else if ((style & Font.BOLD) > 0) { + name = "Times-Bold"; + } else if ((style & Font.ITALIC) > 0) { + name = "Times-Italic"; + } else { + name = "Times-Roman"; + } + } else { + if (((style & Font.BOLD) > 0) && ((style & Font.ITALIC) > 0)) { + name = "Helvetica-BoldOblique"; + } else if ((style & Font.BOLD) > 0) { + name = "Helvetica-Bold"; + } else if ((style & Font.ITALIC) > 0) { + name = "Helvetica-Oblique"; + } else { + name = "Helvetica"; + } + } + + parseFont(name); + } + + /** + * Parse a font given only the name of a builtin font + */ + private void parseFont(String baseFont) throws IOException { + // load the base fonts properties files, if it isn't already loaded + if (props == null) { + props = new Properties(); + props.load(BuiltinFont.class.getResourceAsStream("/com/github/librepdf/pdfrenderer/font/res/BaseFonts.properties")); + } + + // make sure we're a known font + if (!props.containsKey(baseFont + ".file")) { + throw new IllegalArgumentException("Unknown Base Font: " + baseFont); + } + + // get the font information from the properties file + String file = props.getProperty(baseFont + ".file"); + + // the size of the file + int length = Integer.parseInt(props.getProperty(baseFont + ".length")); + // the size of the unencrypted portion + int length1 = 0; + // the size of the encrypted portion + int length2 = 0; + + // read the data from the file + byte[] data = new byte[length]; + InputStream fontStream = NativeFont.class.getResourceAsStream("res/" + file); + int cur = 0; + while (cur < length) { + cur += fontStream.read(data, cur, length - cur); + } + fontStream.close(); + + // are we a pfb file? + if ((data[0] & 0xff) == 0x80) { + // read lengths from the file + length1 = (data[2] & 0xff); + length1 |= (data[3] & 0xff) << 8; + length1 |= (data[4] & 0xff) << 16; + length1 |= (data[5] & 0xff) << 24; + length1 += 6; + + length2 = (data[length1 + 2] & 0xff); + length2 |= (data[length1 + 3] & 0xff) << 8; + length2 |= (data[length1 + 4] & 0xff) << 16; + length2 |= (data[length1 + 5] & 0xff) << 24; + length1 += 6; + } else { + // get the values from the properties file + length1 = Integer.parseInt(props.getProperty(baseFont + ".length1")); + + if (props.containsKey(baseFont + ".length2")) { + length2 = Integer.parseInt(props.getProperty(baseFont + ".lenth2")); + } else { + length2 = length - length1; + } + } + + parseFont(data, length1, length2); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/CIDFontType0.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/CIDFontType0.java new file mode 100644 index 0000000000..caa9a549ff --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/CIDFontType0.java @@ -0,0 +1,68 @@ +package com.github.librepdf.pdfrenderer.font; + +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.font.cid.PDFCMap; +import com.github.librepdf.pdfrenderer.font.ttf.AdobeGlyphList; +import com.github.librepdf.pdfrenderer.PDFObject; + +/***************************************************************************** + * At the moment this is not fully supported to parse CID based fonts + * As a hack we try to use a built in font as substitution and use a + * toUnicode map to translate the characters if available. + * + * + * @version $Id: CIDFontType0.java,v 1.1 2011-08-03 15:48:56 bros Exp $ + * @author Bernd Rosstauscher + * @since 03.08.2011 + ****************************************************************************/ + +public class CIDFontType0 extends BuiltinFont { + + private PDFCMap glyphLookupMap; + + /************************************************************************* + * Constructor + * @param baseFont + * @param fontObj + * @param descriptor + * @throws IOException + ************************************************************************/ + + public CIDFontType0(String baseFont, PDFObject fontObj, + PDFFontDescriptor descriptor) throws IOException { + super(baseFont, fontObj, descriptor); + } + + /************************************************************************* + * @param fontObj + * @throws IOException + ************************************************************************/ + + public void parseToUnicodeMap(PDFObject fontObj) throws IOException { + PDFObject toUnicode = fontObj.getDictRef("ToUnicode"); + if (toUnicode != null) { + PDFCMap cmap = PDFCMap.getCMap(toUnicode); + this.glyphLookupMap = cmap; + } + } + + /** + * Get a character from the first font in the descendant fonts array + */ + @Override + protected PDFGlyph getGlyph(char src, String name) { + //TODO BROS 03.08.2011 Hack for unsupported Type0 CID based fonts + // If we have a toUnicodeMap then try to use that one when mapping to our build in font. + // See "9.10 Extraction of Text Content" in the PDF spec. + if (this.glyphLookupMap != null) { + src = this.glyphLookupMap.map(src); + //The preferred method of getting the glyph should be by name. + if (name == null && src != 160){//unless it NBSP + //so, try to find the name by the char + name = AdobeGlyphList.getGlyphName(src); + } + } + return super.getGlyph(src, name); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/CIDFontType2.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/CIDFontType2.java new file mode 100644 index 0000000000..4e3bd166ed --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/CIDFontType2.java @@ -0,0 +1,272 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.font; + +import java.awt.geom.GeneralPath; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; + +import com.github.librepdf.pdfrenderer.PDFObject; + +/** + * a font object derived from a CID font. + * + * @author Jonathan Kaplan + */ +public class CIDFontType2 extends TTFFont { + + /** + * The width of each glyph from the DW and W arrays + */ + private Map widths = null; + /** + * The vertical width of each glyph from the DW2 and W2 arrays + */ + private Map widthsVertical = null; + + /* + * the default width + */ + private int defaultWidth = 1000; + /* + * the default vertical width + */ + private int defaultWidthVertical = 1000; + /** the CIDtoGID map, if any */ + private ByteBuffer cidToGidMap; + + /** + * create a new CIDFontType2 object based on the name of a built-in font + * and the font descriptor + * @param baseName the name of the font, from the PDF file + * @param fontObj a dictionary that contains the DW (defaultWidth) and + * W (width) parameters + * @param descriptor a descriptor for the font + */ + public CIDFontType2(String baseName, PDFObject fontObj, + PDFFontDescriptor descriptor) throws IOException { + super(baseName, fontObj, descriptor); + + parseWidths(fontObj); + + // read the CIDSystemInfo dictionary (required) + PDFObject systemInfoObj = fontObj.getDictRef("CIDSystemInfo"); + // read the cid to gid map (optional) + PDFObject mapObj = fontObj.getDictRef("CIDToGIDMap"); + + + // only read the map if it is a stream (if it is a name, it + // is "Identity" and can be ignored + if (mapObj != null && (mapObj.getType() == PDFObject.STREAM)) { + this.cidToGidMap = mapObj.getStreamBuffer(); + } + } + + /** Parse the Widths array and DW object */ + private void parseWidths(PDFObject fontObj) + throws IOException { + // read the default width (otpional) + PDFObject defaultWidthObj = fontObj.getDictRef("DW"); + if (defaultWidthObj != null && defaultWidthObj.getIntValue() != 0) { + // XOND: commented out the setting of new default width, as several + // PDFs are displayed in a wrong format due to this: +// this.defaultWidth = defaultWidthObj.getIntValue(); + } + + int entryIdx = 0; + int first = 0; + int last = 0; + PDFObject[] widthArray; + + // read the widths table + PDFObject widthObj = fontObj.getDictRef("W"); + if (widthObj != null) { + + // initialize the widths array + this.widths = new HashMap(); + + // parse the width array + widthArray = widthObj.getArray(); + + /* an entry can be in one of two forms: + * or + * [ array of values ] + * we use the entryIdx to differentitate between them + */ + for (int i = 0; i < widthArray.length; i++) { + if (entryIdx == 0) { + // first value in an entry. Just store it + first = widthArray[i].getIntValue(); + } else if (entryIdx == 1) { + // second value -- is it an int or array? + if (widthArray[i].getType() == PDFObject.ARRAY) { + // add all the entries in the array to the width array + PDFObject[] entries = widthArray[i].getArray(); + for (int c = 0; c < entries.length; c++) { + Character key = Character.valueOf((char) (c + first)); + + // value is width / default width + float value = entries[c].getIntValue(); + this.widths.put(key, new Float(value)); + } + // all done + entryIdx = -1; + } else { + last = widthArray[i].getIntValue(); + } + } else { + // third value. Set a range + int value = widthArray[i].getIntValue(); + + // set the range + for (int c = first; c <= last; c++) { + this.widths.put(Character.valueOf((char) c), new Float(value)); + } + + // all done + entryIdx = -1; + } + + entryIdx++; + } + } + + // read the optional vertical default width + defaultWidthObj = fontObj.getDictRef("DW2"); + if (defaultWidthObj != null) { + this.defaultWidthVertical = defaultWidthObj.getIntValue(); + } + + // read the vertical widths table + widthObj = fontObj.getDictRef("W2"); + if (widthObj != null) { + + // initialize the widths array + this.widthsVertical = new HashMap(); + + // parse the width2 array + widthArray = widthObj.getArray(); + + /* an entry can be in one of two forms: + * or + * [ array of values ] + * we use the entryIdx to differentitate between them + */ + entryIdx = 0; + first = 0; + last = 0; + + for (int i = 0; i < widthArray.length; i++) { + if (entryIdx == 0) { + // first value in an entry. Just store it + first = widthArray[i].getIntValue(); + } else if (entryIdx == 1) { + // second value -- is it an int or array? + if (widthArray[i].getType() == PDFObject.ARRAY) { + // add all the entries in the array to the width array + PDFObject[] entries = widthArray[i].getArray(); + for (int c = 0; c < entries.length; c++) { + Character key = Character.valueOf((char) (c + first)); + + // value is width / default width + float value = entries[c].getIntValue(); + this.widthsVertical.put(key, new Float(value)); + } + // all done + entryIdx = -1; + } else { + last = widthArray[i].getIntValue(); + } + } else { + // third value. Set a range + int value = widthArray[i].getIntValue(); + + // set the range + for (int c = first; c <= last; c++) { + this.widthsVertical.put(Character.valueOf((char) c), new Float(value)); + } + + // all done + entryIdx = -1; + } + + entryIdx++; + } + } + } + + /** Get the default width in text space */ + @Override + public int getDefaultWidth() { + return this.defaultWidth; + } + + /** Get the width of a given character */ + @Override + public float getWidth(char code, String name) { + if (this.widths == null) { + return 1f; + } + Float w = this.widths.get(Character.valueOf(code)); + if (w == null) { + return 1f; + } + + return w.floatValue() / getDefaultWidth(); + } + + /** Get the default vertical width in text space */ + public int getDefaultWidthVertical() { + return this.defaultWidthVertical; + } + + /** Get the vertical width of a given character */ + public float getWidthVertical(char code, String name) { + if (this.widthsVertical == null) { + return 1f; + } + Float w = this.widthsVertical.get(Character.valueOf(code)); + if (w == null) { + return 1f; + } + + return w.floatValue() / getDefaultWidthVertical(); + } + + /** + * Get the outline of a character given the character code. We + * interpose here in order to avoid using the CMap of the font in + * a CID mapped font. + */ + @Override + protected synchronized GeneralPath getOutline(char src, float width) { + int glyphId = (src & 0xffff); + + // check if there is a cidToGidMap + if (this.cidToGidMap != null) { + // read the map + glyphId = this.cidToGidMap.getChar(glyphId * 2); + } + + // call getOutline on the glyphId + return getOutline(glyphId, width); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/FlPoint.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/FlPoint.java new file mode 100644 index 0000000000..c25c31159c --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/FlPoint.java @@ -0,0 +1,48 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.font; + +/** + * A floating-point Point, with public fields. Also contains a flag + * for "open" to indicate that the path this point is a member of has + * or hasn't been closed. + * + * @author Mike Wessler + */ +public class FlPoint { + /** x coordinate of the point */ + public float x= 0; + + /** y coordinate of the point */ + public float y= 0; + + /** + * whether the path this point is a part of is open or closed. + * used in Type1CFont.java. + */ + public boolean open= false; + + /** reset the values to (0,0) and closed */ + public final void reset() { + this.x= 0; + this.y= 0; + this.open= false; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/FontSupport.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/FontSupport.java new file mode 100644 index 0000000000..ad9553cefc --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/FontSupport.java @@ -0,0 +1,416 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.font; + +/** + * some constants and utility functions for font support. + * @author Mike Wessler + */ +public class FontSupport { + + /** + * names for glyphs in the standard Adobe order. This is the ordering + * of the glyphs in a font, not the mapping of character number to + * character. + */ + public static final String stdNames[] = { + ".notdef", "space", "exclam", "quotedbl", "numbersign", "dollar", + "percent", "ampersand", "quoteright", "parenleft", "parenright", + "asterisk", "plus", "comma", "hyphen", "period", "slash", "zero", + "one", "two", "three", "four", "five", "six", "seven", "eight", + "nine", "colon", "semicolon", "less", "equal", "greater", "question", + "at", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", + "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", + "bracketleft", "backslash", "bracketright", "asciicircum", + "underscore", "quoteleft", "a", "b", "c", "d", "e", "f", "g", "h", + "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", + "w", "x", "y", "z", "braceleft", "bar", "braceright", "asciitilde", + "exclamdown", "cent", "sterling", "fraction", "yen", "florin", + "section", "currency", "quotesingle", "quotedblleft", "guillemotleft", + "guilsinglleft", "guilsinglright", "fi", "fl", "endash", "dagger", + "daggerdbl", "periodcentered", "paragraph", "bullet", + "quotesinglbase", "quotedblbase", "quotedblright", "guillemotright", + "ellipsis", "perthousand", "questiondown", "grave", "acute", + "circumflex", "tilde", "macron", "breve", "dotaccent", "dieresis", + "ring", "cedilla", "hungarumlaut", "ogonek", "caron", "emdash", "AE", + "ordfeminine", "Lslash", "Oslash", "OE", "ordmasculine", "ae", + "dotlessi", "lslash", "oslash", "oe", "germandbls", "onesuperior", + "logicalnot", "mu", "trademark", "Eth", "onehalf", "plusminus", + "Thorn", "onequarter", "divide", "brokenbar", "degree", "thorn", + "threequarters", "twosuperior", "registered", "minus", "eth", + "multiply", "threesuperior", "copyright", "Aacute", "Acircumflex", + "Adieresis", "Agrave", "Aring", "Atilde", "Ccedilla", "Eacute", + "Ecircumflex", "Edieresis", "Egrave", "Iacute", "Icircumflex", + "Idieresis", "Igrave", "Ntilde", "Oacute", "Ocircumflex", "Odieresis", + "Ograve", "Otilde", "Scaron", "Uacute", "Ucircumflex", "Udieresis", + "Ugrave", "Yacute", "Ydieresis", "Zcaron", "aacute", "acircumflex", + "adieresis", "agrave", "aring", "atilde", "ccedilla", "eacute", + "ecircumflex", "edieresis", "egrave", "iacute", "icircumflex", + "idieresis", "igrave", "ntilde", "oacute", "ocircumflex", "odieresis", + "ograve", "otilde", "scaron", "uacute", "ucircumflex", "udieresis", + "ugrave", "yacute", "ydieresis", "zcaron", "exclamsmall", + "Hungarumlautsmall", "dollaroldstyle", "dollarsuperior", + "ampersandsmall", "Acutesmall", "parenleftsuperior", + "parenrightsuperior", "twodotenleader", "onedotenleader", + "zerooldstyle", "oneoldstyle", "twooldstyle", "threeoldstyle", + "fouroldstyle", "fiveoldstyle", "sixoldstyle", "sevenoldstyle", + "eightoldstyle", "nineoldstyle", "commasuperior", + "threequartersemdash", "periodsuperior", "questionsmall", "asuperior", + "bsuperior", "centsuperior", "dsuperior", "esuperior", "isuperior", + "lsuperior", "msuperior", "nsuperior", "osuperior", "rsuperior", + "ssuperior", "tsuperior", "ff", "ffi", "ffl", "parenleftinferior", + "parenrightinferior", "Circumflexsmall", "hyphensuperior", + "Gravesmall", "Asmall", "Bsmall", "Csmall", "Dsmall", "Esmall", + "Fsmall", "Gsmall", "Hsmall", "Ismall", "Jsmall", "Ksmall", "Lsmall", + "Msmall", "Nsmall", "Osmall", "Psmall", "Qsmall", "Rsmall", "Ssmall", + "Tsmall", "Usmall", "Vsmall", "Wsmall", "Xsmall", "Ysmall", "Zsmall", + "colonmonetary", "onefitted", "rupiah", "Tildesmall", + "exclamdownsmall", "centoldstyle", "Lslashsmall", "Scaronsmall", + "Zcaronsmall", "Dieresissmall", "Brevesmall", "Caronsmall", + "Dotaccentsmall", "Macronsmall", "figuredash", "hypheninferior", + "Ogoneksmall", "Ringsmall", "Cedillasmall", "questiondownsmall", + "oneeighth", "threeeighths", "fiveeighths", "seveneighths", + "onethird", "twothirds", "zerosuperior", "foursuperior", + "fivesuperior", "sixsuperior", "sevensuperior", "eightsuperior", + "ninesuperior", "zeroinferior", "oneinferior", "twoinferior", + "threeinferior", "fourinferior", "fiveinferior", "sixinferior", + "seveninferior", "eightinferior", "nineinferior", "centinferior", + "dollarinferior", "periodinferior", "commainferior", "Agravesmall", + "Aacutesmall", "Acircumflexsmall", "Atildesmall", "Adieresissmall", + "Aringsmall", "AEsmall", "Ccedillasmall", "Egravesmall", + "Eacutesmall", "Ecircumflexsmall", "Edieresissmall", "Igravesmall", + "Iacutesmall", "Icircumflexsmall", "Idieresissmall", "Ethsmall", + "Ntildesmall", "Ogravesmall", "Oacutesmall", "Ocircumflexsmall", + "Otildesmall", "Odieresissmall", "OEsmall", "Oslashsmall", + "Ugravesmall", "Uacutesmall", "Ucircumflexsmall", "Udieresissmall", + "Yacutesmall", "Thornsmall", "Ydieresissmall", "001.000", "001.001", + "001.002", "001.003", "Black", "Bold", "Book", "Light", "Medium", + "Regular", "Roman", "Semibold" + }; + + /** + * characters for glyphs in the standard order. These are string "values" + * to go with the names in stdNames. Not all glyphs have been translated + * to their unicode values. In many cases, the name of the glyph has + * been appended to an ASCII approximation of the glyph. Strings longer + * than 3 characters have this characteristic. To get the character, + * use the string if it contains 3 or fewer characters; otherwise, + * grab the first character off the string and use that. + */ + static final String stdValues[] = { + "", " ", "!", "\"", "#", "$", + "%", "&", "'", "(", ")", + "*", "+", ",", "-", ".", "/", "0", + "1", "2", "3", "4", "5", "6", "7", "8", + "9", ":", ";", "<", "=", ">", "?", + "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", + "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", + "[", "\\", "]", "^", + "_", "`", "a", "b", "c", "d", "e", "f", "g", "h", + "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", + "w", "x", "y", "z", "{", "|", "}", "~", + "\u00a1", "\u00a2", "\u00a3", "/fraction", "\u00a5", "Fflorin", + "\u00a7", "\u00a4", "\u00b4quotesingle", "\u201c", "?guillemotleft", + "\u2039", "\u203a", "fi", "fl", "--", "\u2020", + "\u2021", "\u00b7", "\u00b6", "\u2022", + "'quotesinglbase", "\"quotedblbase", "\"quotedblright", "?guillemotright", + "...ellipsis", "%perthousand", "?questiondown", "`grave", "'acute", + "^circumflex", "~tilde", "-macron", "?breve", "?dotaccent", "?dieresis", + "oring", "ccedilla", ":hungarumlaut", "?ogonek", ",caron", "---emdash", "AE", + "aordfeminine", "LLslash", "OOslash", "OE", "oordmasculine", "ae", + "idotlessi", "llslash", "ooslash", "oe", "Bgermandbls", "1onesuperior", + "~logicalnot", "?mu", "(TM)trademark", "?Eth", "1/2", "+/-", + "?Thorn", "1/4", "/divide", "|brokenbar", "*degree", "?thorn", + "3/4", "2twosuperior", "(R)", "-minus", "?eth", + "*multiply", "3threesuperior", "(C)", "AAacute", "AAcircumflex", + "AAdieresis", "AAgrave", "AAring", "AAtilde", "CCcedilla", "EEacute", + "EEcircumflex", "EEdieresis", "EEgrave", "IIacute", "IIcircumflex", + "IIdieresis", "IIgrave", "NNtilde", "OOacute", "OOcircumflex", "OOdieresis", + "OOgrave", "OOtilde", "SScaron", "UUacute", "UUcircumflex", "UUdieresis", + "UUgrave", "YYacute", "YYdieresis", "ZZcaron", "aaacute", "aacircumflex", + "aadieresis", "aagrave", "aaring", "aatilde", "cccedilla", "eeacute", + "eecircumflex", "eedieresis", "eegrave", "iiacute", "iicircumflex", + "iidieresis", "iigrave", "nntilde", "ooacute", "oocircumflex", "oodieresis", + "oograve", "ootilde", "sscaron", "uuacute", "uucircumflex", "uudieresis", + "uugrave", "yyacute", "yydieresis", "zzcaron", "!exclamsmall", + "?Hungarumlautsmall", "$dollaroldstyle", "$dollarsuperior", + "&ersandsmall", "'Acutesmall", "/parenleftsuperior", + "\\parenrightsuperior", "?twodotenleader", "?onedotenleader", + "0zerooldstyle", "1oneoldstyle", "2twooldstyle", "3threeoldstyle", + "4fouroldstyle", "5fiveoldstyle", "6sixoldstyle", "7sevenoldstyle", + "8eightoldstyle", "9nineoldstyle", "'commasuperior", + "--threequartersemdash", ".periodsuperior", "?questionsmall", "aasuperior", + "bbsuperior", "ccentsuperior", "ddsuperior", "eesuperior", "iisuperior", + "llsuperior", "mmsuperior", "nnsuperior", "oosuperior", "rrsuperior", + "sssuperior", "ttsuperior", "ff", "ffi", "ffl", "\\parenleftinferior", + "/parenrightinferior", "^Circumflexsmall", "-hyphensuperior", + "`Gravesmall", "AAsmall", "BBsmall", "CCsmall", "DDsmall", "EEsmall", + "FFsmall", "GGsmall", "HHsmall", "IIsmall", "JJsmall", "KKsmall", "LLsmall", + "MMsmall", "NNsmall", "OOsmall", "PPsmall", "QQsmall", "RRsmall", "SSsmall", + "TTsmall", "UUsmall", "VVsmall", "WWsmall", "XXsmall", "YYsmall", "ZZsmall", + ":colonmonetary", "1onefitted", "?rupiah", "~Tildesmall", + "!exclamdownsmall", "ccentoldstyle", "LLslashsmall", "SScaronsmall", + "ZZcaronsmall", "?Dieresissmall", "?Brevesmall", "^Caronsmall", + "?Dotaccentsmall", "?Macronsmall", "--figuredash", "-hypheninferior", + "?Ogoneksmall", "oRingsmall", ",Cedillasmall", "?questiondownsmall", + "1/8oneeighth", "3/8threeeighths", "5/8fiveeighths", "7/8seveneighths", + "1/3onethird", "2/3twothirds", "0zerosuperior", "4foursuperior", + "5fivesuperior", "6sixsuperior", "7sevensuperior", "8eightsuperior", + "9ninesuperior", "0zeroinferior", "1oneinferior", "2twoinferior", + "3threeinferior", "4fourinferior", "5fiveinferior", "6sixinferior", + "7seveninferior", "8eightinferior", "9nineinferior", "ccentinferior", + "$dollarinferior", ".periodinferior", ",commainferior", "AAgravesmall", + "AAacutesmall", "AAcircumflexsmall", "AAtildesmall", "AAdieresissmall", + "AAringsmall", "AEAEsmall", "CCcedillasmall", "EEgravesmall", + "EEacutesmall", "EEcircumflexsmall", "EEdieresissmall", "IIgravesmall", + "IIacutesmall", "IIcircumflexsmall", "IIdieresissmall", "EthEthsmall", + "NNtildesmall", "OOgravesmall", "OOacutesmall", "OOcircumflexsmall", + "OOtildesmall", "OOdieresissmall", "OEOEsmall", "OOslashsmall", + "UUgravesmall", "UUacutesmall", "UUcircumflexsmall", "UUdieresissmall", + "YYacutesmall", "?Thornsmall", "YYdieresissmall", "?001.000", "?001.001", + "?001.002", "?001.003", " Black", " Bold", " Book", " Light", " Medium", + " Regular", " Roman", " Semibold", + /* extra mac stuff */ + "?NUL", "?HT", " LF", " CR", "?DLE", "?DC1", "?DC2", "?DC3", "?DC4", "?RS", + "?US", "!=", "?DEL", "?infinity", "<=", ">=", + "?partialdiff", "?summation", "xproduct", "?pi", "?integral", "?Omega", + "?radical", "~=", "?Delta", " nbspace", "?lozenge", "?apple" + }; + + /** + * glyph order of the glyphs for the Type1C Expert character set. These + * are indices into the glyph name array. + */ + public static final int type1CExpertCharset[] = { + 1, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 13, 14, 15, 99, + 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 27, 28, 249, 250, + 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, + 265, 266, 109, 110, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, + 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, + 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, + 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, + 158, 155, 163, 319, 320, 321, 322, 323, 324, 325, 326, 150, 164, 169, + 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, + 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, + 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, + 369, 370, 371, 372, 373, 374, 375, 376, 377, 378 + }; + + /** + * glyph order of the glyphs for the Type1C Expert Sub character set. + * These are indices into the glyph name array. + */ + public static final int type1CExpertSubCharset[] = { + 1, 231, 232, 235, 236, 237, 238, 13, 14, 15, 99, 239, 240, 241, 242, + 243, 244, 245, 246, 247, 248, 27, 28, 249, 250, 251, 253, 254, 255, + 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 109, 110, 267, + 268, 269, 270, 272, 300, 301, 302, 305, 314, 315, 158, 155, 163, 320, + 321, 322, 323, 324, 325, 326, 150, 164, 169, 327, 328, 329, 330, 331, + 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, + 346 + }; + + /** + * extra names for the Macintosh glyph set. This array should be + * considered to be appended to the stdNames array. The stdValues array + * already contains values for this set. + */ + public static final String macExtras[] = { // index starts at 391=NUL + "NUL", "HT", "LF", "CR", "DLE", "DC1", "DC2", "DC3", "DC4", "RS", + "US", "notequal", "DEL", "infinity", "lessequal", "greaterequal", + "partialdiff", "summation", "product", "pi", "integral", "Omega", + "radical", "approxequal", "Delta", "nbspace", "lozenge", "apple" + }; + + /** + * character mapping from values to glyphs for the Macintosh MacRoman + * encoding + */ + public static final int macRomanEncoding[] = { + 391, 154, 167, 140, 146, 192, 221, 197, 226, 392, 393, 157, 162, 394, + 199, 228, 395, 396, 397, 398, 399, 155, 158, 150, 163, 169, 164, 160, + 166, 168, 400, 401, 1, 2, 3, 4, 5, 6, 7, 104, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, + 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 124, + 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, + 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 403, 173, 175, + 177, 178, 186, 189, 195, 200, 203, 201, 202, 205, 204, 206, 207, 210, + 208, 209, 211, 214, 212, 213, 215, 216, 219, 217, 218, 220, 222, 225, + 223, 224, 112, 161, 97, 98, 102, 116, 115, 149, 165, 170, 153, 125, + 131, 402, 138, 141, 404, 156, 405, 406, 100, 152, 407, 408, 409, 410, + 411, 139, 143, 412, 144, 147, 123, 96, 151, 413, 101, 414, 415, 106, + 120, 121, 416, 174, 176, 191, 142, 148, 111, 137, 105, 119, 65, 8, + 159, 417, 227, 198, 99, 103, 107, 108, 109, 110, 113, 114, 117, 118, + 122, 172, 179, 171, 180, 181, 182, 183, 184, 185, 187, 188, 418, 190, + 193, 194, 196, 145, 126, 127, 128, 129, 130, 132, 133, 134, 135, 136 + }; + + /** + * character mapping from values to glyphs for the isoLatin1Encoding + */ + public static final int isoLatin1Encoding[] = { + 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, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, + 166, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, + 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, + 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 145, 124, 125, 126, 127, 128, + 129, 130, 131, 0, 132, 133, 0, 134, 135, 136, 1, 96, 97, 98, 103, + 100, 160, 102, 131, 170, 139, 106, 151, 14, 165, 128, 161, 156, 164, + 169, 125, 152, 115, 114, 133, 150, 143, 120, 158, 155, 163, 123, 174, + 171, 172, 176, 173, 175, 138, 177, 181, 178, 179, 180, 185, 182, 183, + 184, 154, 186, 190, 187, 188, 191, 189, 168, 141, 196, 193, 194, 195, + 197, 157, 149, 203, 200, 201, 205, 202, 204, 144, 206, 210, 207, 208, + 209, 214, 211, 212, 213, 167, 215, 219, 216, 217, 220, 218, 159, 147, + 225, 222, 223, 224, 226, 162, 227 + }; + + /** + * character mapping from values to glyphs for the Windows winAnsi + * character encoding + */ + public static final int winAnsiEncoding[] = { + 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 145, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, + 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, + 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, + 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, + 92, 93, 94, 95, 0, 0, 0, 117, 101, 118, 121, 112, 113, 0, 122, 192, + 107, 142, 0, 0, 0, 0, 65, 8, 105, 119, 116, 111, 137, 0, 153, 221, + 108, 148, 0, 0, 198, 1, 96, 97, 98, 103, 100, 160, 102, 131, 170, + 139, 106, 151, 14, 165, 128, 161, 156, 164, 169, 125, 152, 115, 114, + 133, 150, 143, 120, 158, 155, 163, 123, 174, 171, 172, 176, 173, 175, + 138, 177, 181, 178, 179, 180, 185, 182, 183, 184, 154, 186, 190, 187, + 188, 191, 189, 168, 141, 196, 193, 194, 195, 197, 157, 149, 203, 200, + 201, 205, 202, 204, 144, 206, 210, 207, 208, 209, 214, 211, 212, 213, + 167, 215, 219, 216, 217, 220, 218, 159, 147, 225, 222, 223, 224, 226, + 162, 227 + }; + + /** + * character mapping from values to glyphs for Adobe's standard + * character encoding + */ + public static final int standardEncoding[] = { + 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, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, + 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, + 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 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, 0, 0, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, + 106, 107, 108, 109, 110, 0, 111, 112, 113, 114, 0, 115, 116, 117, + 118, 119, 120, 121, 122, 0, 123, 0, 124, 125, 126, 127, 128, 129, + 130, 131, 0, 132, 133, 0, 134, 135, 136, 137, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 138, 0, 139, 0, 0, 0, 0, 140, 141, 142, 143, + 0, 0, 0, 0, 0, 144, 0, 0, 0, 145, 0, 0, 146, 147, 148, 149, 0, 0, 0, + 0 + }; + + /** + * Character mapping from values to glyphs for the symbol set encoding + * Definition found here: PDF specification Annex D.5 Symbol Set and Encoding + */ + public static final int symbolSetEncoding[] = { + 101, 102, 103, 104, 105, 110, 240, 107, 301, 111, 113, 114, 115, 116, + 127, 117, 106, 120, 131, 302, 122, 123, 124, 121, 125, 241, 130, 132, + 300, 141, 046, 320, 341, 361, 273, 253, 333, 337, 334, 336, 335, 257, + 276, 254, 256, 255, 275, 052, 174, 142, 173, 175, 354, 355, 356, 374, + 375, 376, 357, 133, 135, 351, 352, 353, 371, 372, 373, 267, 277, 143, + 304, 305, 247, 072, 054, 100, 343, 323, 260, 144, 250, 270, 327, 070, + 316, 274, 306, 145, 075, 272, 150, 041, 044, 065, 246, 064, 244, 147, + 321, 076, 263, 251, 245, 362, 363, 364, 365, 307, 151, 153, 154, 074, + 243, 331, 330, 332, 340, 055, 242, 155, 264, 071, 317, 271, 313, 156, + 043, 167, 166, 157, 061, 050, 051, 346, 347, 350, 366, 367, 370, 266, + 045, 056, 136, 146, 152, 160, 053, 261, 325, 314, 311, 265, 171, 077, + 326, 140, 315, 312, 342, 322, 162, 262, 073, 067, 163, 126, 176, 066, + 057, 040, 252, 047, 345, 164, 134, 161, 112, 063, 344, 324, 062, 137, + 310, 042, 165, 303, 170, 060, 172 + }; + + /** + * get the name of a glyph from its encoding value (NOT the character + * value), using the standard encoding. + */ + public static String getName (int i) { + if (i < stdNames.length) { + return stdNames[i]; + } else { + i -= stdNames.length; + if (i < macExtras.length) { + return macExtras[i]; + } + } + return ".notdef"; + } + + /** + * get the encoding value a glyph given its name and a name table. + * @param name the name of the glyph + * @param table the charset as an array of names + * @return the index of the name in the table, or -1 if the name + * cannot be found in the table + */ + public static int findName (String name, String[] table) { + for (int i = 0; i < table.length; i++) { + if (name.equals (table[i])) { + return i; + } + } + return -1; + } + + /** + * get the encoding value of a glyph given its name and a charset. + * @param name the name of the glyph + * @param table the charset table + * @return the index of the name in the charset. + */ + public static int findName (String name, int[] table) { + for (int i = 0; i < table.length; i++) { + if (name.equals (getName (table[i]))) { + return i; + } + } + return -1; + } + + /** + * get the encoding value of a glyph given its name, in the standard + * charset. This is equivalent to findName(name, FontSupport.stdNames). + * @param name the name of the glyph + * @return the index of the name in stdNames, or -1 if the name doesn't + * appear in stdNames. + */ + public static int getStrIndex (String name) { + for (int i = 0; i < stdNames.length; i++) { + if (name.equals (stdNames[i])) { + return i; + } + } + return -1; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/NativeFont.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/NativeFont.java new file mode 100644 index 0000000000..64ad790b48 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/NativeFont.java @@ -0,0 +1,442 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.font; + +import java.awt.Font; +import java.awt.FontFormatException; +import java.awt.font.FontRenderContext; +import java.awt.font.GlyphVector; +import java.awt.font.OpenType; +import java.awt.geom.AffineTransform; +import java.awt.geom.GeneralPath; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +import com.github.librepdf.pdfrenderer.BaseWatchable; +import com.github.librepdf.pdfrenderer.PDFDebugger; +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; +import com.github.librepdf.pdfrenderer.font.ttf.CMap; +import com.github.librepdf.pdfrenderer.font.ttf.CMapFormat0; +import com.github.librepdf.pdfrenderer.font.ttf.CMapFormat4; +import com.github.librepdf.pdfrenderer.font.ttf.CmapTable; +import com.github.librepdf.pdfrenderer.font.ttf.HeadTable; +import com.github.librepdf.pdfrenderer.font.ttf.HmtxTable; +import com.github.librepdf.pdfrenderer.font.ttf.NameTable; +import com.github.librepdf.pdfrenderer.font.ttf.PostTable; +import com.github.librepdf.pdfrenderer.font.ttf.TrueTypeFont; +import com.github.librepdf.pdfrenderer.font.ttf.TrueTypeTable; + +/** + * a font object derived from a true type font. + * + * @author Mike Wessler + */ +public class NativeFont extends OutlineFont { + + /** Control characters to filter out of the underlying font */ + protected static final char[] controlChars = {0x9, 0xa, 0xd}; + + /** the ids of our favorite CMaps */ + protected static final short[] mapIDs = { + 3, 1, /* Microsoft Unicode */ + 0, 0, /* unicode default */ + 0, 3, /* unicode 2.0 map */ + 1, 0 /* macintosh */}; + + /** the actual font in use */ + private Font f; + + /** the font render context */ + private final FontRenderContext basecontext = + new FontRenderContext (new AffineTransform (), + true, true); + + /** the cmap table from a TrueType font */ + private CmapTable cmapTable; + + /** the post table from a TrueType font */ + private PostTable postTable; + + /** the number of font units in one em */ + private int unitsPerEm; + + /** the hmtx table from the TrueType font */ + private HmtxTable hmtxTable; + + /** + * create a new NativeFont object based on a description of the + * font from the PDF file. If the description happens to contain + * an in-line true-type font file (under key "FontFile2"), use the + * true type font. Otherwise, parse the description for key information + * and use that to generate an appropriate font. + */ + public NativeFont (String baseFont, PDFObject fontObj, + PDFFontDescriptor descriptor) + throws IOException { + super (baseFont, fontObj, descriptor); + + String fontName = descriptor.getFontName (); + + PDFObject ttf = descriptor.getFontFile2 (); + if (ttf != null) { + byte[] fontdata = ttf.getStream (); + + try { + setFont (fontdata); + } catch (FontFormatException ffe) { + throw new PDFParseException ("Font format exception: " + ffe); + } + } else { + int flags = descriptor.getFlags (); + int style = ((flags & PDFFontDescriptor.FORCEBOLD) != 0) ? Font.BOLD : Font.PLAIN; + + if (fontName.indexOf ("Bold") > 0) { + style |= Font.BOLD; + } + if (descriptor.getItalicAngle () != 0) { + style |= Font.ITALIC; + } + if ((flags & PDFFontDescriptor.FIXED_PITCH) != 0) { // fixed width + setFont (new Font ("Monospaced", style, 1)); + } else if ((flags & PDFFontDescriptor.SERIF) != 0) { // serif font + setFont (new Font ("Serif", style, 1)); + } else { + setFont (new Font ("Sans-serif", style, 1)); + } + } + } + + /** + * Get a glyph outline by name + * + * @param name the name of the desired glyph + * @return the glyph outline, or null if unavailable + */ + @Override + protected GeneralPath getOutline (String name, float width) { + if (this.postTable != null && this.cmapTable != null) { + // map this character name to a glyph ID + short glyphID = this.postTable.getGlyphNameIndex (name); + + if (glyphID == 0) { + // no glyph -- try by index + return null; + } + + // the mapped character + char mappedChar = 0; + + for (int i = 0; i < mapIDs.length; i += 2) { + CMap map = this.cmapTable.getCMap (mapIDs[i], mapIDs[i + 1]); + if (map != null) { + mappedChar = map.reverseMap (glyphID); + + // we found a character + if (mappedChar != 0) { + break; + } + } + } + + return getOutline (mappedChar, width); + } + + // no maps found, hope the font can deal + return null; + } + + /** + * Get a glyph outline by character code + * + * Note this method must always return an outline + * + * @param src the character code of the desired glyph + * @return the glyph outline + */ + @Override + protected GeneralPath getOutline (char src, float width) { + // some true type fonts put characters in the undefined + // region of Unicode instead of as normal characters. + if (!this.f.canDisplay (src) && this.f.canDisplay ((char) (src + 0xf000))) { + src += 0xf000; + } + + // filter out control characters + for (int i = 0; i < controlChars.length; i++) { + if (controlChars[i] == src) { + src = (char) (0xf000 | src); + break; + } + } + + char[] glyph = new char[1]; + glyph[0] = src; + + GlyphVector gv = this.f.createGlyphVector (this.basecontext, glyph); + GeneralPath gp = new GeneralPath (gv.getGlyphOutline (0)); + + // this should be gv.getGlyphMetrics(0).getAdvance(), but that is + // broken on the Mac, so we need to read the advance from the + // hmtx table in the font + CMap map = this.cmapTable.getCMap (mapIDs[0], mapIDs[1]); + int glyphID = map.map (src); + float advance = (float) this.hmtxTable.getAdvance (glyphID) / (float) this.unitsPerEm; + + float widthfactor = width / advance; + gp.transform (AffineTransform.getScaleInstance (widthfactor, -1)); + + return gp; + } + + /** + * Set the font + * + * @param f the font to use + */ + protected void setFont (Font f) { + this.f = f; + + // if it's an OpenType font, parse the relevant tables to get + // glyph name to code mappings + if (f instanceof OpenType) { + OpenType ot = (OpenType) f; + + byte[] cmapData = ot.getFontTable (OpenType.TAG_CMAP); + byte[] postData = ot.getFontTable (OpenType.TAG_POST); + + TrueTypeFont ttf = new TrueTypeFont (0x10000); + + this.cmapTable = + (CmapTable) TrueTypeTable.createTable (ttf, "cmap", + ByteBuffer.wrap (cmapData)); + ttf.addTable ("cmap", this.cmapTable); + + this.postTable = + (PostTable) TrueTypeTable.createTable (ttf, "post", + ByteBuffer.wrap (postData)); + ttf.addTable ("post", this.postTable); + } + } + + /** + * Set the font + * + * @param fontdata the font data as a byte array + */ + protected void setFont (byte[] fontdata) + throws FontFormatException, IOException { + try { + // read the true type information + TrueTypeFont ttf = TrueTypeFont.parseFont (fontdata); + + // get the cmap, post, and hmtx tables for later use + this.cmapTable = (CmapTable) ttf.getTable ("cmap"); + this.postTable = (PostTable) ttf.getTable ("post"); + this.hmtxTable = (HmtxTable) ttf.getTable ("hmtx"); + + // read the units per em from the head table + HeadTable headTable = (HeadTable) ttf.getTable ("head"); + this.unitsPerEm = headTable.getUnitsPerEm (); + + /* Find out if we have the right info in our name table. + * This is a hack because Java can only deal with fonts that + * have a Microsoft encoded name in their name table (PlatformID 3). + * We'll 'adjust' the font to add it if not, and take our chances + * with our parsing, since it wasn't going to work anyway. + */ + NameTable nameTable = null; + + try { + nameTable = (NameTable) ttf.getTable ("name"); + } catch (Exception ex) { + PDFDebugger.debug("Error reading name table for font " + getBaseFont () + ". Repairing!"); + } + + boolean nameFixed = fixNameTable (ttf, nameTable); + + /* Figure out if we need to hack the CMap table. This might + * be the case if we use characters that Java considers control + * characters (0x9, 0xa and 0xd), that have to be re-mapped + */ + boolean cmapFixed = fixCMapTable (ttf, this.cmapTable); + + // use the parsed font instead of the original + if (nameFixed || cmapFixed) { + fontdata = ttf.writeFont (); + } + } catch (Exception ex) { + PDFDebugger.debug("Error parsing font : " + getBaseFont ()); + BaseWatchable.getErrorHandler().publishException(ex); + } + + ByteArrayInputStream bais = new ByteArrayInputStream (fontdata); + this.f = Font.createFont (Font.TRUETYPE_FONT, bais); + bais.close (); + } + + /** + * Fix a broken font name table for a TrueType font. Some fonts do not + * have Microsoft-specific name information, but Java won't work without + * it (grrr.). This method takes a font and adds the Microsoft data into + * it. + * + * @param ttf the font + * @param name the font's name table + * @return true if the table was fixed, or false if it was left as is + */ + private boolean fixNameTable (TrueTypeFont ttf, NameTable name) { + // if we didn't find the table, or there was an exception, + // just create a new one + if (name == null) { + name = (NameTable) TrueTypeTable.createTable (ttf, "name"); + ttf.addTable ("name", name); + } + + // first, figure out some info about the font + String fName = this.getBaseFont (); + String style = "Regular"; + + if (fName.indexOf ("Italic") > -1 || fName.indexOf ("italic") > -1) { + style = "Italic"; + } else if (fName.indexOf ("Bold") > -1 || fName.indexOf ("bold") > -1) { + style = "Bold"; + } + + if (fName.indexOf ('-') > -1) { + fName = fName.substring (0, fName.indexOf ('-')); + } + + short platID = NameTable.PLATFORMID_MICROSOFT; + short encID = 1; + short langID = 1033; + + short[] nameIDs = { + NameTable.NAMEID_COPYRIGHT, + NameTable.NAMEID_FAMILY, + NameTable.NAMEID_SUBFAMILY, + NameTable.NAMEID_SUBFAMILY_UNIQUE, + NameTable.NAMEID_FULL_NAME, + NameTable.NAMEID_VERSION, + NameTable.NAMEID_POSTSCRIPT_NAME, + NameTable.NAMEID_TRADEMARK + }; + + String[] defaultValues = { + "No copyright", + fName, + style, + fName + " " + style, + fName + " " + style, + "1.0 (Fake)", + fName, + "No Trademark" + }; + + boolean changed = false; + + for (int i = 0; i < nameIDs.length; i++) { + if (name.getRecord (platID, encID, langID, nameIDs[i]) == null) { + name.addRecord (platID, encID, langID, nameIDs[i], + defaultValues[i]); + changed = true; + } + } + + return changed; + } + + /** + * Fix the CMap table. This can be necessary if characters are mapped to + * control characters (0x9, 0xa, 0xd) Java will not render them, even + * though they are valid. + * + * Also, Java tends to not like it when there is only a Format 0 CMap, + * which happens frequently when included Format 4 CMaps are broken. + * Since PDF prefers the Format 0 map, while Java prefers the Format 4 map, + * it is generally necessary to re-write the Format 0 map as a Format 4 map + * to make most PDFs work. + * + * @param ttf the font + * @param cmap the CMap table + * @return true if the font was changed, or false if it was left as-is + */ + private boolean fixCMapTable (TrueTypeFont ttf, CmapTable cmap) { + CMapFormat4 fourMap = null; + CMapFormat0 zeroMap = null; + + for (int i = 0; i < mapIDs.length; i += 2) { + CMap map = this.cmapTable.getCMap (mapIDs[i], mapIDs[i + 1]); + if (map != null) { + if (fourMap == null && map instanceof CMapFormat4) { + fourMap = (CMapFormat4) map; + } else if (zeroMap == null && map instanceof CMapFormat0) { + zeroMap = (CMapFormat0) map; + } + } + } + + // if there were no maps, we could have problems. Just try creating + // an identity map + if (zeroMap == null && fourMap == null) { + fourMap = (CMapFormat4) CMap.createMap ((short) 4, (short) 0); + fourMap.addSegment ((short) getFirstChar (), + (short) getLastChar (), + (short) 0); + } + + // create our map based on the type 0 map, since PDF seems + // to prefer a type 0 map (Java prefers a unicode map) + if (zeroMap != null) { + fourMap = (CMapFormat4) CMap.createMap ((short) 4, (short) 0); + + // add the mappings from 0 to null and 1 to notdef + fourMap.addSegment ((short) 0, (short) 1, (short) 0); + + for (int i = getFirstChar (); i <= getLastChar (); i++) { + short value = (short) (zeroMap.map ((byte) i) & 0xff); + if (value != 0) { + fourMap.addSegment ((short) i, (short) i, + (short) (value - i)); + } + } + } + + // now that we have a type four map, remap control characters + for (int i = 0; i < controlChars.length; i++) { + short idx = (short) (0xf000 | controlChars[i]); + short value = (short) fourMap.map (controlChars[i]); + + fourMap.addSegment (idx, idx, (short) (value - idx)); + } + + // create a whole new table with just our map + cmap = (CmapTable) TrueTypeTable.createTable (ttf, "cmap"); + cmap.addCMap ((short) 3, (short) 1, fourMap); + + // replace the table in the font + ttf.addTable ("cmap", cmap); + + // change the stored table + this.cmapTable = cmap; + + return true; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/OutlineFont.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/OutlineFont.java new file mode 100644 index 0000000000..d8d08a58a1 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/OutlineFont.java @@ -0,0 +1,154 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.font; + +import java.awt.geom.GeneralPath; +import java.awt.geom.Point2D; +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.PDFObject; + +/** + * Supports width operations for Type1, Type1C, TrueType and Type3 fonts + */ +public abstract class OutlineFont extends PDFFont { + + /** the first character code */ + private int firstChar = -1; + /** the last character code */ + private int lastChar = -1; + /** the widths for each character code */ + private float[] widths; + + /** Creates a new instance of OutlineFont */ + public OutlineFont(String baseFont, PDFObject fontObj, + PDFFontDescriptor descriptor) throws IOException { + super(baseFont, descriptor); + + PDFObject firstCharObj = fontObj.getDictRef("FirstChar"); + PDFObject lastCharObj = fontObj.getDictRef("LastChar"); + PDFObject widthArrayObj = fontObj.getDictRef("Widths"); + + if (firstCharObj != null) { + this.firstChar = firstCharObj.getIntValue(); + } + if (lastCharObj != null) { + this.lastChar = lastCharObj.getIntValue(); + } + + if (widthArrayObj != null) { + PDFObject[] widthArray = widthArrayObj.getArray(); + + this.widths = new float[widthArray.length]; + + for (int i = 0; i < widthArray.length; i++) { + this.widths[i] = widthArray[i].getFloatValue() / getDefaultWidth(); + } + } + } + + /** Get the first character code */ + public int getFirstChar() { + return this.firstChar; + } + + /** Get the last character code */ + public int getLastChar() { + return this.lastChar; + } + + /** Get the default width in text space */ + public int getDefaultWidth() { + return 1000; + } + + /** Get the number of characters */ + public int getCharCount() { + return (getLastChar() - getFirstChar()) + 1; + } + + /** Get the width of a given character */ + public float getWidth(char code, String name) { + int idx = (code & 0xff) - getFirstChar(); + + // make sure we're in range + if (idx < 0 || this.widths == null || idx >= this.widths.length) { + // try to get the missing width from the font descriptor + if (getDescriptor() != null) { + return getDescriptor().getMissingWidth() / (float)getDefaultWidth(); + } else { + return 0; + } + } + + return this.widths[idx]; + } + + /** + * Get the glyph for a given character code and name + * + * The preferred method of getting the glyph should be by name. If the + * name is null or not valid, then the character code should be used. + * If the both the code and the name are invalid, the undefined glyph + * should be returned. + * + * Note this method must *always* return a glyph. + * + * @param src the character code of this glyph + * @param name the name of this glyph or null if unknown + * @return a glyph for this character + */ + @Override + protected PDFGlyph getGlyph(char src, String name) { + GeneralPath outline = null; + float width = getWidth(src, name); + + // first try by name + if (name != null) { + outline = getOutline(name, width); + } + + // now try by character code (guaranteed to return) + if (outline == null) { + outline = getOutline(src, width); + } + + // calculate the advance + Point2D.Float advance = new Point2D.Float(width, 0); + return new PDFGlyph(src, name, outline, advance); + } + + /** + * Get a glyph outline by name + * + * @param name the name of the desired glyph + * @return the glyph outline, or null if unavailable + */ + protected abstract GeneralPath getOutline(String name, float width); + + /** + * Get a glyph outline by character code + * + * Note this method must always return an outline + * + * @param src the character code of the desired glyph + * @return the glyph outline + */ + protected abstract GeneralPath getOutline(char src, float width); +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/PDFFont.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/PDFFont.java new file mode 100644 index 0000000000..5c132cfbd5 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/PDFFont.java @@ -0,0 +1,528 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.font; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import com.github.librepdf.pdfrenderer.font.cid.PDFCMap; +import com.github.librepdf.pdfrenderer.font.ttf.TrueTypeFont; +import com.github.librepdf.pdfrenderer.BaseWatchable; +import com.github.librepdf.pdfrenderer.PDFDebugger; +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/** + * a Font definition for PDF files + * @author Mike Wessler + */ +public abstract class PDFFont { + + private static final FilenameFilter TTF_FILTER = new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.toLowerCase().endsWith(".ttf"); + } + }; + + private static Map namedFontsToLocalTtfFiles = null; + + /** the font SubType of this font */ + private String subtype; + /** the postscript name of this font */ + private String baseFont; + /** the font encoding (maps character ids to glyphs) */ + private PDFFontEncoding encoding; + /** the font descriptor */ + private PDFFontDescriptor descriptor; + /** the CMap that maps this font to unicode values */ + private PDFCMap unicodeMap; + /** a cache of glyphs indexed by character */ + private Map charCache; + + + + /** + * get the PDFFont corresponding to the font described in a PDFObject. + * The object is actually a dictionary containing the following keys:
    + * Type = "Font"
    + * Subtype = (Type1 | TrueType | Type3 | Type0 | MMType1 | CIDFontType0 | + * CIDFontType2)
    + * FirstChar = #
    + * LastChar = #
    + * Widths = array of #
    + * Encoding = (some name representing a dictionary in the resources | an + * inline dictionary) + *

    + * For Type1 and TrueType fonts, the dictionary also contains:
    + * BaseFont = (some name, or XXXXXX+Name as a subset of font Name) + *

    + * For Type3 font, the dictionary contains:
    + * FontBBox = (rectangle)
    + * FontMatrix = (array, typically [0.001, 0, 0, 0.001, 0, 0])
    + * CharProcs = (dictionary) + * Resources = (dictionary) + */ + public synchronized static PDFFont getFont(PDFObject obj, + HashMap resources) + throws IOException { + // the obj is actually a dictionary containing: + // Type (=Font) + // Subtype (Type1, TrueType, Type3, Type0, MMType1, CIDFontType0,2) + // FirstChar (int) + // LastChar (int) + // Widths (array) + // Encoding (name or dict) : assumes StandardEncoding + // and........ + // Type1 and TrueType fonts: + // BaseFont (name) // may be XXXXXX+Fontname as a subset. + // FontDescriptor (dict) + // Type3 fonts: + // FontBBox (rectangle) + // FontMatrix (array) // e.g. [0.001 0 0 0.001 0 0] + // CharProcs (dict) + // Resources (dict) + // + // Font descriptor (Type1 and TrueType fonts): + // FontName (name) + // Flags (1=monospace, 2=serif, 4=script, 7=italic, 19=bold) + // FontBBox (rectangle) + // ItalicAngle (float) + // Ascent (float) + // Descent (float) + // CapHeight (float) + // StemV (float) + // FontFile (stream for Type1 fonts) + // FontFile2 (stream for TrueType fonts) + // FontFile3 (stream for CFF/Type1C fonts) + // + // Font data can be Type1, TrueType(native), or Type1C + PDFFont font = (PDFFont) obj.getCache(); + if (font != null) { + return font; + } + + String baseFont = null; + PDFFontEncoding encoding = null; + PDFFontDescriptor descriptor = null; + + String subType = obj.getDictRef("Subtype").getStringValue(); + if (subType == null) { + subType = obj.getDictRef("S").getStringValue(); + } + PDFObject baseFontObj = obj.getDictRef("BaseFont"); + PDFObject encodingObj = obj.getDictRef("Encoding"); + PDFObject descObj = obj.getDictRef("FontDescriptor"); + + if (baseFontObj != null) { + baseFont = baseFontObj.getStringValue(); + } else { + baseFontObj = obj.getDictRef("Name"); + if (baseFontObj != null) { + baseFont = baseFontObj.getStringValue(); + } + } + + if (encodingObj != null) { + encoding = new PDFFontEncoding(subType, encodingObj); + } + + if (descObj != null) { + descriptor = new PDFFontDescriptor(descObj, subType); + } else { + descriptor = new PDFFontDescriptor(baseFont); + } + + if (subType.equals("Type0")) { + font = new Type0Font(baseFont, obj, descriptor); + } else if (subType.equals("Type1")) { + // load a type1 font + if (descriptor.getFontFile() != null) { + // it's a Type1 font, included. + font = new Type1Font(baseFont, obj, descriptor); + if(!((Type1Font)font).isName2OutlineFilled()){ + PDFDebugger.debug("Type1Font can't be parsed completelly, character mapping missing. Use a basefont instead."); + font = new BuiltinFont(baseFont, obj, descriptor); + } + } else if (descriptor.getFontFile3() != null) { + // it's a CFF (Type1C) font + font = new Type1CFont(baseFont, obj, descriptor); + } else { + // no font info. Fake it based on the FontDescriptor + font = new BuiltinFont(baseFont, obj, descriptor); + } + } else if (subType.equals("TrueType")) { + if (descriptor.getFontFile2() != null) { + // load a TrueType font + try { + font = new TTFFont(baseFont, obj, descriptor); + }catch (Exception e) { +// PDFRenderer.getErrorHandler().publishException(e); + PDFDebugger.debug("Error parsing font : " + baseFont); + // fake it with a built-in font + font = new BuiltinFont(baseFont, obj, descriptor); + } + } else { + final File extFontFile = findExternalTtf(baseFont); + if (extFontFile != null) { + try { + font = new TTFFont(baseFont, obj, descriptor, extFontFile); + }catch (Exception e) { +// PDFRenderer.getErrorHandler().publishException(e); + PDFDebugger.debug("Error parsing font : " + baseFont); + // fake it with a built-in font + font = new BuiltinFont(baseFont, obj, descriptor); + } + } else { + // fake it with a built-in font + font = new BuiltinFont(baseFont, obj, descriptor); + } + } + } else if (subType.equals("Type3")) { + // load a type 3 font + font = new Type3Font(baseFont, obj, resources, descriptor); + } else if (subType.equals("CIDFontType2")) { + if(descriptor.getFontFile2() != null) { + font = new CIDFontType2(baseFont, obj, descriptor); + }else { + // fake it with a built-in font + //but it prefer to use the CIDFontType0 that have the extra handling of ToUnicode, if found in the fontObj + font = new CIDFontType0(baseFont, obj, descriptor); + } + } else if (subType.equals("CIDFontType0")) { + if(descriptor.getFontFile2() !=null){ + font = new CIDFontType2(baseFont, obj, descriptor); + }else { + font = new CIDFontType0(baseFont, obj, descriptor); + } + } else if (subType.equals("MMType1")) { + // not yet implemented, fake it with a built-in font + font = new BuiltinFont(baseFont, obj, descriptor); + } else { + throw new PDFParseException("Don't know how to handle a '" + + subType + "' font"); + } + + font.setSubtype(subType); + font.setEncoding(encoding); + + obj.setCache(font); + return font; + } + + private static File findExternalTtf(String fontName) { + ensureNamedTtfFontFiles(); + return namedFontsToLocalTtfFiles.get(fontName); + } + + private synchronized static void ensureNamedTtfFontFiles() { + if (namedFontsToLocalTtfFiles == null) { + namedFontsToLocalTtfFiles = new HashMap(); + + if (Boolean.getBoolean("PDFRenderer.avoidExternalTtf")) { + return; + } + + for (final String fontDirName : getFontSearchPath()) { + + final File fontDir = new File(fontDirName); + if (fontDir.exists()) { + for (final File ttfFile : fontDir.listFiles(TTF_FILTER)) { + if (ttfFile.canRead()) { + try { + byte[] fontBytes; + RandomAccessFile fontRa = null; + try { + fontRa = new RandomAccessFile (ttfFile, "r"); + int size = (int) fontRa.length (); + fontBytes = new byte[size]; + fontRa.readFully(fontBytes); + } finally { + if (fontRa != null) { + fontRa.close(); + } + } + + TrueTypeFont ttf = TrueTypeFont.parseFont(fontBytes); + for (final String fontName : ttf.getNames()) { + if (!namedFontsToLocalTtfFiles.containsKey(fontName)) { + namedFontsToLocalTtfFiles.put(fontName, ttfFile); + } + } + } catch (Throwable t) { + // I'm not sure how much confidence we should have + // in the font parsing, so we'll avoid relying on + // this not to fail + System.err.println("Problem parsing " + ttfFile); + BaseWatchable.getErrorHandler().publishException(t); + } + } + } + } + } + } + + } + + private static String[] getFontSearchPath() { + String pathProperty = System.getProperty("PDFRenderer.fontSearchPath"); + if (pathProperty != null) { + return pathProperty.split(Pattern.quote(File.pathSeparator)); + } else { + return getDefaultFontSearchPath(); + } + } + + + private static String[] getDefaultFontSearchPath() + { + String osName = null; + try { + osName = System.getProperty("os.name"); + } catch (SecurityException e) { + // preserve null osName + } + + if (osName == null) { + // Makes it a bit tricky to figure out a nice default + return new String[0]; + } + + osName = osName != null ? osName.toLowerCase() : ""; + if (osName.startsWith("windows")) { + // start with some reasonable default + String path = "C:/WINDOWS/Fonts"; + try { + String windir = System.getenv("WINDIR"); + if (windir != null) { + path = windir + "/Fonts/"; + } + } catch (SecurityException secEx) { + // drop through and accept default path + } + return new String[] { path }; + } else if (osName != null && osName.startsWith("mac")) { + List paths = new ArrayList(Arrays.asList( + "/Library/Fonts", + "/Network/Library/Fonts", + "/System/Library/Fonts", + "/System Folder/Fonts")); + // try and add the user font dir at the front + try { + paths.add(0, System.getProperty("user.home") + "/Library/Fonts"); + } catch (SecurityException e) { + // I suppose we just won't use the user fonts + } + return paths.toArray(new String[paths.size()]); + } else { + // Feel free to insert some reasonable defaults for other + // (UNIX, most likely) platforms here + return new String[0]; + } + } + + /** + * Get the subtype of this font. + * @return the subtype, one of: Type0, Type1, TrueType or Type3 + */ + public String getSubtype() { + return this.subtype; + } + + /** + * Set the font subtype + */ + public void setSubtype(String subtype) { + this.subtype = subtype; + } + + /** + * Get the postscript name of this font + * @return the postscript name of this font + */ + public String getBaseFont() { + return this.baseFont; + } + + /** + * Set the postscript name of this font + * @param baseFont the postscript name of the font + */ + public void setBaseFont(String baseFont) { + this.baseFont = baseFont; + } + + /** + * Get the encoding for this font + * @return the encoding which maps from this font to actual characters + */ + public PDFFontEncoding getEncoding() { + return this.encoding; + } + + /** + * Set the encoding for this font + */ + public void setEncoding(PDFFontEncoding encoding) { + this.encoding = encoding; + } + + /** + * Get the descriptor for this font + * @return the font descriptor + */ + public PDFFontDescriptor getDescriptor() { + return this.descriptor; + } + + /** + * Set the descriptor font descriptor + */ + public void setDescriptor(PDFFontDescriptor descriptor) { + this.descriptor = descriptor; + } + + /** + * Get the CMap which maps the characters in this font to unicode names + */ + public PDFCMap getUnicodeMap() { + return this.unicodeMap; + } + + /** + * Set the CMap which maps the characters in this font to unicode names + */ + public void setUnicodeMap(PDFCMap unicodeMap) { + this.unicodeMap = unicodeMap; + } + + /** + * Get the glyphs associated with a given String in this font + * + * @param text the text to translate into glyphs + */ + public List getGlyphs(String text) { + List outList = null; + + // if we have an encoding, use it to get the commands + // don't use the encoding if it is "OneByteIdentityH" (hack for case #205739) + if (this.encoding != null && !this.encoding.isOneByteIdentity()) { + outList = this.encoding.getGlyphs(this, text); + } else { + // use the default mapping + char[] arry = text.toCharArray(); + outList = new ArrayList(arry.length); + + for (int i = 0; i < arry.length; i++) { + // only look at 2 bytes when there is no encoding + char src = (char) (arry[i] & 0xff); + outList.add(getCachedGlyph(src, null)); + } + } + + return outList; + } + + /** + * Get a glyph for a given character code. The glyph is returned + * from the cache if available, or added to the cache if not + * + * @param src the character code of this glyph + * @param name the name of the glyph, or null if the name is unknown + * @return a glyph for this character + */ + public PDFGlyph getCachedGlyph(char src, String name) { + if (this.charCache == null) { + this.charCache = new HashMap(); + } + + // try the cache + PDFGlyph glyph = this.charCache.get(Character.valueOf(src)); + + // if it's not there, add it to the cache + if (glyph == null) { + glyph = getGlyph(src, name); + this.charCache.put(Character.valueOf(src), glyph); + } + + return glyph; + } + + /** + * Create a PDFFont given the base font name and the font descriptor + * @param baseFont the postscript name of this font + * @param descriptor the descriptor for the font + */ + protected PDFFont(String baseFont, PDFFontDescriptor descriptor) { + setBaseFont(baseFont); + setDescriptor(descriptor); + } + + /** + * Get the glyph for a given character code and name + * + * The preferred method of getting the glyph should be by name. If the + * name is null or not valid, then the character code should be used. + * If the both the code and the name are invalid, the undefined glyph + * should be returned. + * + * Note this method must *always* return a glyph. + * + * @param src the character code of this glyph + * @param name the name of this glyph or null if unknown + * @return a glyph for this character + */ + protected abstract PDFGlyph getGlyph(char src, String name); + + /** + * Turn this font into a pretty String + */ + @Override + public String toString() { + return getBaseFont(); + } + + /** + * Compare two fonts base on the baseFont + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof PDFFont)) { + return false; + } + + return ((PDFFont) o).getBaseFont().equals(getBaseFont()); + } + + /** + * Hash a font based on its base font + */ + @Override + public int hashCode() { + return getBaseFont().hashCode(); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/PDFFontDescriptor.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/PDFFontDescriptor.java new file mode 100644 index 0000000000..40fe064395 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/PDFFontDescriptor.java @@ -0,0 +1,539 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.font; + +import java.awt.geom.Rectangle2D; +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.PDFObject; + +/** + * + * @author jkaplan + */ +public class PDFFontDescriptor { + + /** All glyphs have the same width. */ + public final static int FIXED_PITCH = 1 << (1-1); + /** Glyphs have serifs. */ + public final static int SERIF = 1 << (2-1); + /** Font contains glyphs outside the Adobe standard Latin. */ + public final static int SYMBOLIC = 1 << (3-1); + /** Glyphs resemble cursive handwriting. */ + public final static int SCRIPT = 1 << (4-1); + /** Font uses the Adobe standard Latic character set. */ + public final static int NONSYMBOLIC = 1 << (6-1); + /** Glyphs have dominant vertical strokes that are slanted. */ + public final static int ITALIC = 1 << (7-1); + /** Font contains no lowercase letters. */ + public final static int ALLCAP = 1 << (17-1); + /** Font contains both uppercase and lowercase letters.. */ + public final static int SMALLCAP = 1 << (18-1); + /** Determines whether bold glyphs shall be painted with + * extra pixels even at very small text sizes. */ + public final static int FORCEBOLD = 1 << (19-1); + /** Holds value of property ascent. */ + private int ascent; + /** Holds value of property capHeight. */ + private int capHeight; + /** Holds value of property descent. */ + private int descent; + /** Holds value of property flags. */ + private int flags; + /** Holds the optional FontFamily (PDF 1.5) */ + private String fontFamily; + /** Holds value of property fontName. */ + private String fontName; + /** Holds the optional FontStretch (PDF 1.5) */ + private String fontStretch; + /** Holds the optional FontWeight (PDF 1.5) */ + private int fontWeight; + /** Holds value of property italicAngle. */ + private int italicAngle = 0; + /** Holds value of property stemV. */ + private int stemV; + /** Holds value of property avgWidth. */ + private int avgWidth = 0; + /** Holds value of property fontFile. */ + private PDFObject fontFile; + /** Holds value of property fontFile2. */ + private PDFObject fontFile2; + /** Holds value of property fontFile3. */ + private PDFObject fontFile3; + /** Holds value of property leading. */ + private int leading = 0; + /** Holds value of property maxWidth. */ + private int maxWidth = 0; + /** Holds value of property misingWidth. */ + private int missingWidth = 0; + /** Holds value of property stemH. */ + private int stemH = 0; + /** Holds value of property xHeight. */ + private int xHeight = 0; + /** Holds value of property charSet. */ + private PDFObject charSet; + /** Holds value of property fontBBox. */ + private Rectangle2D.Float fontBBox; + + /** Creates a new instance of PDFFontDescriptor */ + public PDFFontDescriptor(String basefont) { + setFontName(basefont); + // [[MW TODO: find basefont info and fill in the rest of the + // descriptor?]] + } + + /** Creates a new instance of PDFFontDescriptor */ + public PDFFontDescriptor(PDFObject obj, String fontSubType) throws IOException { + // required parameters + setFlags(obj.getDictRef("Flags").getIntValue()); + PDFObject fontNameObj = obj.getDictRef("FontName"); + if (fontNameObj == null){ + // fallback to avoid NPE try to use the BaseFont + fontNameObj = obj.getDictRef("BaseFont"); + } + setFontName(fontNameObj.getStringValue()); + setItalicAngle(obj.getDictRef("ItalicAngle").getIntValue()); + + // conditionally required parameters + boolean areConditionalParametersRequired = !"Type3".equals(fontSubType) + && !Boolean.getBoolean("PDFRenderer.lenientFontDescriptorParsing"); + + // these values are declared as Required except for Type 3 fonts + // however a value might not be available for some fonts and + // therefore some predefined value is set, so that we have a fallback + if ( obj.getDictionary().containsKey("Ascent")) { + setAscent(obj.getDictRef("Ascent").getIntValue()); + } + else if (areConditionalParametersRequired) { + setAscent(728); // value of ArialMT as used with Report Label + } + if ( obj.getDictionary().containsKey("CapHeight")) { + setCapHeight(obj.getDictRef("CapHeight").getIntValue()); + } + else if (areConditionalParametersRequired) { + setCapHeight(716); // value of ArialMT as used with Report Label + } + if ( obj.getDictionary().containsKey("Descent")) { + setDescent(obj.getDictRef("Descent").getIntValue()); + } + else if (areConditionalParametersRequired) { + setDescent(-210); // value of ArialMT as used with Report Label + } + if ( obj.getDictionary().containsKey("StemV")) { + setStemV(obj.getDictRef("StemV").getIntValue()); + } + else if (areConditionalParametersRequired) { + setStemV(109); // "normal" value for vertical stem width (PDFlib) + } + + // font bounding box (non-optional but a NPE won't help) + if (obj.getDictionary().containsKey("FontBBox")) { + PDFObject[] bboxdef = obj.getDictRef("FontBBox").getArray(); + float[] bboxfdef = new float[4]; + for (int i = 0; i < 4; i++) { + bboxfdef[i] = bboxdef[i].getFloatValue(); + } + setFontBBox(new Rectangle2D.Float(bboxfdef[0], bboxfdef[1], + bboxfdef[2] - bboxfdef[0], + bboxfdef[3] - bboxfdef[1])); + } + + // optional parameters + if (obj.getDictionary().containsKey("AvgWidth")) { + setAvgWidth(obj.getDictRef("AvgWidth").getIntValue()); + } + if (obj.getDictionary().containsKey("FontFile")) { + setFontFile(obj.getDictRef("FontFile")); + } + if (obj.getDictionary().containsKey("FontFile2")) { + setFontFile2(obj.getDictRef("FontFile2")); + } + if (obj.getDictionary().containsKey("FontFile3")) { + setFontFile3(obj.getDictRef("FontFile3")); + } + if (obj.getDictionary().containsKey("Leading")) { + setLeading(obj.getDictRef("Leading").getIntValue()); + } + if (obj.getDictionary().containsKey("MaxWidth")) { + setMaxWidth(obj.getDictRef("MaxWidth").getIntValue()); + } + if (obj.getDictionary().containsKey("MissingWidth")) { + setMissingWidth(obj.getDictRef("MissingWidth").getIntValue()); + } + if (obj.getDictionary().containsKey("StemH")) { + setStemH(obj.getDictRef("StemH").getIntValue()); + } + if (obj.getDictionary().containsKey("XHeight")) { + setXHeight(obj.getDictRef("XHeight").getIntValue()); + } + if (obj.getDictionary().containsKey("CharSet")) { + setCharSet(obj.getDictRef("CharSet")); + } + if (obj.getDictionary().containsKey("FontFamily")) { + setFontFamily(obj.getDictRef("FontFamily").getStringValue()); + } + if (obj.getDictionary().containsKey("FontWeight")) { + setFontWeight(obj.getDictRef("FontWeight").getIntValue()); + } + if (obj.getDictionary().containsKey("FontStretch")) { + setFontStretch(obj.getDictRef("FontStretch").getStringValue()); + } + } + + /** Getter for property ascent. + * @return Value of property ascent. + * + */ + public int getAscent() { + return this.ascent; + } + + /** Setter for property ascent. + * @param ascent New value of property ascent. + * + */ + public void setAscent(int ascent) { + this.ascent = ascent; + } + + /** Getter for property capHeight. + * @return Value of property capHeight. + * + */ + public int getCapHeight() { + return this.capHeight; + } + + /** Setter for property capHeight. + * @param capHeight New value of property capHeight. + * + */ + public void setCapHeight(int capHeight) { + this.capHeight = capHeight; + } + + /** Getter for property descent. + * @return Value of property descent. + * + */ + public int getDescent() { + return this.descent; + } + + /** Setter for property descent. + * @param descent New value of property descent. + * + */ + public void setDescent(int descent) { + this.descent = descent; + } + + /** Getter for property flags. + * @return Value of property flags. + * + */ + public int getFlags() { + return this.flags; + } + + /** Setter for property flags. + * @param flags New value of property flags. + * + */ + public void setFlags(int flags) { + this.flags = flags; + } + + /** Getter for property fontFamily. Option (PDF 1.5) + * @return Value of the property fontFamily + */ + public String getFontFamily() { + return this.fontFamily; + } + + /** Setter for property fontFamily. + * @param fontFamily New value of property fontFamily. + * + */ + public void setFontFamily(String fontFamily) { + this.fontFamily = fontFamily; + } + + /** Getter for property fontName. + * @return Value of property fontName. + * + */ + public String getFontName() { + return this.fontName; + } + + /** Setter for property fontName. + * @param fontName New value of property fontName. + * + */ + public void setFontName(String fontName) { + this.fontName = fontName; + } + + /** Getter for property fontStretch. Option (PDF 1.5) + * + * @return Value of the property fontStretch + */ + public String getFontStretch() { + return this.fontStretch; + } + + /** Setter for property fontStretch. Possible values are: + * UltraCondensed, ExtraCondensed, Condensed, SemiCondensed, + * Normal, SemiExpanded, Expanded, ExtraExpanded or UltraExpanded + * We do not check at this time. + * + * @param fontStretch New value of property fontStretch. + * + */ + public void setFontStretch(String fontStretch) { + this.fontStretch = fontStretch; + } + + /** Getter for property fontWeight. Option (PDF 1.5) + * + * @return Value of the property fontWeight + */ + public int getFontWeight() { + return this.fontWeight; + } + + /** Setter for property fontWeight. Possible values are: + * 100, 200, 300, 400, 500, 600, 700, 800, 900 + * We do not check at this time. + * + * @param fontWeight New value of property fontWeight. + * + */ + public void setFontWeight(int fontWeight) { + this.fontWeight = fontWeight; + } + + /** Getter for property italicAngle. + * @return Value of property italicAngle. + * + */ + public int getItalicAngle() { + return this.italicAngle; + } + + /** Setter for property italicAngle. + * @param italicAngle New value of property italicAngle. + * + */ + public void setItalicAngle(int italicAngle) { + this.italicAngle = italicAngle; + } + + /** Getter for property stemV. + * @return Value of property stemV. + * + */ + public int getStemV() { + return this.stemV; + } + + /** Setter for property stemV. + * @param stemV New value of property stemV. + * + */ + public void setStemV(int stemV) { + this.stemV = stemV; + } + + /** Getter for property avgWidth. + * @return Value of property avgWidth. + * + */ + public int getAvgWidth() { + return this.avgWidth; + } + + /** Setter for property avgWidth. + * @param avgWidth New value of property avgWidth. + * + */ + public void setAvgWidth(int avgWidth) { + this.avgWidth = avgWidth; + } + + /** Getter for property fontFile. + * @return Value of property fontFile. + * + */ + public PDFObject getFontFile() { + return this.fontFile; + } + + /** Setter for property fontFile. + * @param fontFile New value of property fontFile. + * + */ + public void setFontFile(PDFObject fontFile) { + this.fontFile = fontFile; + } + + /** Getter for property fontFile2. + * @return Value of property fontFile2. + * + */ + public PDFObject getFontFile2() { + return this.fontFile2; + } + + /** Setter for property fontFile2. + * @param fontFile2 New value of property fontFile2. + * + */ + public void setFontFile2(PDFObject fontFile2) { + this.fontFile2 = fontFile2; + } + + /** Getter for property fontFile3. + * @return Value of property fontFile3. + * + */ + public PDFObject getFontFile3() { + return this.fontFile3; + } + + /** Setter for property fontFile3. + * @param fontFile3 New value of property fontFile3. + * + */ + public void setFontFile3(PDFObject fontFile3) { + this.fontFile3 = fontFile3; + } + + /** Getter for property leading. + * @return Value of property leading. + * + */ + public int getLeading() { + return this.leading; + } + + /** Setter for property leading. + * @param leading New value of property leading. + * + */ + public void setLeading(int leading) { + this.leading = leading; + } + + /** Getter for property maxWidth. + * @return Value of property maxWidth. + * + */ + public int getMaxWidth() { + return this.maxWidth; + } + + /** Setter for property maxWidth. + * @param maxWidth New value of property maxWidth. + * + */ + public void setMaxWidth(int maxWidth) { + this.maxWidth = maxWidth; + } + + /** Getter for property misingWidth. + * @return Value of property misingWidth. + * + */ + public int getMissingWidth() { + return this.missingWidth; + } + + /** Setter for property misingWidth. + * @param missingWidth New value of property misingWidth. + */ + public void setMissingWidth(int missingWidth) { + this.missingWidth = missingWidth; + } + + /** Getter for property stemH. + * @return Value of property stemH. + * + */ + public int getStemH() { + return this.stemH; + } + + /** Setter for property stemH. + * @param stemH New value of property stemH. + * + */ + public void setStemH(int stemH) { + this.stemH = stemH; + } + + /** Getter for property xHeight. + * @return Value of property xHeight. + * + */ + public int getXHeight() { + return this.xHeight; + } + + /** Setter for property xHeight. + * @param xHeight New value of property xHeight. + * + */ + public void setXHeight(int xHeight) { + this.xHeight = xHeight; + } + + /** Getter for property charSet. + * @return Value of property charSet. + * + */ + public PDFObject getCharSet() { + return this.charSet; + } + + /** Setter for property charSet. + * @param charSet New value of property charSet. + * + */ + public void setCharSet(PDFObject charSet) { + this.charSet = charSet; + } + + /** Getter for property fontBBox. + * @return Value of property fontBBox. + * + */ + public Rectangle2D.Float getFontBBox() { + return this.fontBBox; + } + + /** Setter for property fontBBox. + * @param fontBBox New value of property fontBBox. + * + */ + public void setFontBBox(Rectangle2D.Float fontBBox) { + this.fontBBox = fontBBox; + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/PDFFontEncoding.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/PDFFontEncoding.java new file mode 100644 index 0000000000..c15fb6aa21 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/PDFFontEncoding.java @@ -0,0 +1,226 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.font; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.github.librepdf.pdfrenderer.font.cid.PDFCMap; +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFRenderer; + +/** + * The PDFFont encoding encapsulates the mapping from character codes + * in the PDF document to glyphs of the font. + * + * Encodings take two basic forms. For Type1, TrueType, and Type3 fonts, + * the encoding maps from character codes to Strings, which represent the + * glyphs of the font. For Type0 fonts, the mapping is a CMap which maps + * character codes to characters in one of many descendant fonts. + * + * Note that the data in the PDF might be ASCII characters (bytes) or it might + * be a multi-byte format such as unicode. For now we will assume all + * glyph ids fit into at most the two bytes of a character. + */ +public class PDFFontEncoding { + + /** Encoding types */ + private static final int TYPE_ENCODING = 0; + private static final int TYPE_CMAP = 1; + /** + * the base encoding (an array of integers which can be mapped to names + * using the methods on FontSupport + */ + private int[] baseEncoding; + /** any differences from the base encoding */ + private Map differences; + /** + * a CMap for fonts encoded by CMap + */ + private PDFCMap cmap; + /** + * the type of this encoding (encoding or CMap) + */ + private int type; + private PDFObject mapName; + + public PDFFontEncoding(PDFCMap cmap) { + super(); + this.type = TYPE_CMAP; + this.cmap = cmap; + } + + /** Creates a new instance of PDFFontEncoding */ + public PDFFontEncoding(String fontType, PDFObject encoding) + throws IOException { + if (encoding.getType() == PDFObject.NAME) { + // if the encoding is a String, it is the name of an encoding + // or the name of a CMap, depending on the type of the font + if (fontType.equals("Type0")) { + this.type = TYPE_CMAP; + this.cmap = PDFCMap.getCMap(encoding.getStringValue()); + } else { + this.type = TYPE_ENCODING; + + this.differences = new HashMap(); + this.baseEncoding = this.getBaseEncoding(encoding.getStringValue()); + } + } else { + // loook at the "Type" entry of the encoding to determine the type + String typeStr = encoding.getDictRef("Type").getStringValue(); + + if (typeStr.equals("Encoding")) { + // it is an encoding + this.type = TYPE_ENCODING; + parseEncoding(encoding); + } else if (typeStr.equals("CMap")) { + // it is a CMap + this.type = TYPE_CMAP; + this.cmap = PDFCMap.getCMap(encoding); + this.mapName = encoding.getDictRef("CMapName"); + } else { + throw new IllegalArgumentException("Uknown encoding type: " + this.type); + } + } + } + + /** Get the glyphs associated with a given String */ + public List getGlyphs(PDFFont font, String text) { + List outList = new ArrayList(text.length()); + + // go character by character through the text + char[] arry = text.toCharArray(); + for (int i = 0; i < arry.length; i++) { + switch (this.type) { + case TYPE_ENCODING: + outList.add(getGlyphFromEncoding(font, arry[i])); + break; + case TYPE_CMAP: + // 2 bytes -> 1 character in a CMap + char c = (char) ((arry[i] & 0xff) << 8); + if (i < arry.length - 1) { + c |= (char) (arry[++i] & 0xff); + } + outList.add(getGlyphFromCMap(font, c)); + break; + } + } + + return outList; + } + + /** + * Get a glyph from an encoding, given a font and character + */ + private PDFGlyph getGlyphFromEncoding(PDFFont font, char src) { + String charName = null; + + // only deal with one byte of source + src &= 0xff; + + // see if this character is in the differences list + if (this.differences.containsKey(Character.valueOf(src))) { + charName = this.differences.get(Character.valueOf(src)); + } else if (this.baseEncoding != null) { + // get the character name from the base encoding + int charID = this.baseEncoding[src]; + charName = FontSupport.getName(charID); + } + + return font.getCachedGlyph(src, charName); + } + + /** + * Get a glyph from a CMap, given a Type0 font and a character + */ + private PDFGlyph getGlyphFromCMap(PDFFont font, char src) { + int fontID = this.cmap.getFontID(src); + char charID = this.cmap.map(src); + + if (font instanceof Type0Font) { + font = ((Type0Font) font).getDescendantFont(fontID); + } + + return font.getCachedGlyph(charID, null); + } + + /** + * Parse a PDF encoding object for the actual encoding + */ + public void parseEncoding(PDFObject encoding) throws IOException { + this.differences = new HashMap(); + + // figure out the base encoding, if one exists + PDFObject baseEncObj = encoding.getDictRef("BaseEncoding"); + if (baseEncObj != null) { + this.baseEncoding = getBaseEncoding(baseEncObj.getStringValue()); + } + + // parse the differences array + PDFObject diffArrayObj = encoding.getDictRef("Differences"); + if (diffArrayObj != null) { + PDFObject[] diffArray = diffArrayObj.getArray(); + int curPosition = -1; + + for (int i = 0; i < diffArray.length; i++) { + if (diffArray[i].getType() == PDFObject.NUMBER) { + curPosition = diffArray[i].getIntValue(); + } else if (diffArray[i].getType() == PDFObject.NAME) { + Character key = Character.valueOf((char) curPosition); + this.differences.put(key, diffArray[i].getStringValue()); + curPosition++; + } else { + throw new IllegalArgumentException("Unexpected type in diff array: " + diffArray[i]); + } + } + } + } + + /** Get the base encoding for a given name */ + private int[] getBaseEncoding(String encodingName) { + if (encodingName.equals("MacRomanEncoding")) { + return FontSupport.macRomanEncoding; + } else if (encodingName.equals("MacExpertEncoding")) { + return FontSupport.type1CExpertCharset; + } else if (encodingName.equals("WinAnsiEncoding")) { + return FontSupport.winAnsiEncoding; + } else if (encodingName.equals("StandardEncoding")) { + return FontSupport.standardEncoding; + } else if(encodingName.equals("SymbolSetEncoding")) { + return FontSupport.symbolSetEncoding; + } else { + throw new IllegalArgumentException("Unknown encoding: " + encodingName); + } + } + + public boolean isOneByteIdentity() { + if(this.mapName != null) { + try { + return "OneByteIdentityH".equals(this.mapName.getStringValue()); + } catch (IOException e) { + PDFRenderer.getErrorHandler().publishException(e); + } + } + + return false; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/PDFGlyph.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/PDFGlyph.java new file mode 100644 index 0000000000..6777911b4e --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/PDFGlyph.java @@ -0,0 +1,101 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.font; + +import java.awt.geom.AffineTransform; +import java.awt.geom.GeneralPath; +import java.awt.geom.Point2D; + +import com.github.librepdf.pdfrenderer.PDFPage; +import com.github.librepdf.pdfrenderer.PDFShapeCmd; + +/** +* A single glyph in a stream of PDF text, which knows how to write itself +* onto a PDF command stream +*/ +public class PDFGlyph { + /** the character code of this glyph */ + private final char src; + /** the name of this glyph */ + private final String name; + /** the advance from this glyph */ + private final Point2D advance; + /** the shape represented by this glyph (for all fonts but type 3) */ + private GeneralPath shape; + /** the PDFPage storing this glyph's commands (for type 3 fonts) */ + private PDFPage page; + + /** Creates a new instance of PDFGlyph based on a shape */ + public PDFGlyph(char src, String name, GeneralPath shape, Point2D.Float advance) { + this.shape = shape; + this.advance = advance; + this.src = src; + this.name = name; + } + + /** Creates a new instance of PDFGlyph based on a page */ + public PDFGlyph(char src, String name, PDFPage page, Point2D advance) { + this.page = page; + this.advance = advance; + this.src = src; + this.name = name; + } + + /** Get the character code of this glyph */ + public char getChar() { + return this.src; + } + + /** Get the name of this glyph */ + public String getName() { + return this.name; + } + + /** Get the shape of this glyph */ + public GeneralPath getShape() { + return this.shape; + } + + /** Get the PDFPage for a type3 font glyph */ + public PDFPage getPage() { + return this.page; + } + + /** Add commands for this glyph to a page */ + public Point2D addCommands(PDFPage cmds, AffineTransform transform, int mode) { + if (this.shape != null) { + GeneralPath outline = (GeneralPath) this.shape.createTransformedShape(transform); + cmds.addCommand(new PDFShapeCmd(outline, mode, false)); + } else if (this.page != null) { + cmds.addCommands(this.page, transform); + } + return this.advance; + } + + public Point2D getAdvance() { + return advance; + } + + @Override + public String toString() { + StringBuffer str = new StringBuffer(); + str.append(this.name); + return str.toString(); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/TTFFont.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/TTFFont.java new file mode 100644 index 0000000000..d326568ee7 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/TTFFont.java @@ -0,0 +1,373 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.font; + +import java.awt.geom.AffineTransform; +import java.awt.geom.GeneralPath; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.util.Collection; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.font.ttf.AdobeGlyphList; +import com.github.librepdf.pdfrenderer.font.ttf.CMap; +import com.github.librepdf.pdfrenderer.font.ttf.CmapTable; +import com.github.librepdf.pdfrenderer.font.ttf.Glyf; +import com.github.librepdf.pdfrenderer.font.ttf.GlyfCompound; +import com.github.librepdf.pdfrenderer.font.ttf.GlyfSimple; +import com.github.librepdf.pdfrenderer.font.ttf.GlyfTable; +import com.github.librepdf.pdfrenderer.font.ttf.HeadTable; +import com.github.librepdf.pdfrenderer.font.ttf.HmtxTable; +import com.github.librepdf.pdfrenderer.font.ttf.PostTable; +import com.github.librepdf.pdfrenderer.font.ttf.TrueTypeFont; + +/** + * A true-type font + */ +public class TTFFont extends OutlineFont { + + /** the truetype font itself */ + private TrueTypeFont font; + + /** the number of units per em in the font */ + private float unitsPerEm; + + public TTFFont (String baseFont, PDFObject fontObj, + PDFFontDescriptor descriptor) throws IOException { + this(baseFont, fontObj, descriptor, null); + } + /** + * create a new TrueTypeFont object based on a description of the + * font from the PDF file. If the description happens to contain + * an in-line true-type font file (under key "FontFile2"), use the + * true type font. Otherwise, parse the description for key information + * and use that to generate an appropriate font. + */ + public TTFFont (String baseFont, PDFObject fontObj, + PDFFontDescriptor descriptor, File fontFile) + throws IOException { + super (baseFont, fontObj, descriptor); + + PDFObject ttfObj = descriptor.getFontFile2 (); + + if (ttfObj != null || fontFile != null) { + if (ttfObj != null) { + font = TrueTypeFont.parseFont (ttfObj.getStreamBuffer ()); + } else { + final RandomAccessFile raFile = fontFile != null ? new RandomAccessFile(fontFile, "r") : null; + final FileChannel fc = raFile.getChannel(); + try { + MappedByteBuffer mappedFont = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size()); + font = TrueTypeFont.parseFont(mappedFont); + mappedFont = null; + } finally { + try { + fc.close(); + } catch (IOException ioEx) { + // swallow + } + try { + raFile.close(); + } catch (IOException ioEx) { + // swallow + } + } + } + // read the units per em from the head table + HeadTable head = (HeadTable) font.getTable ("head"); + unitsPerEm = head.getUnitsPerEm (); + } else { + font = null; + } + } + + public Collection getNames() + { + return font.getNames(); + } + + /** + * Get the outline of a character given the character code + */ + @Override + protected synchronized GeneralPath getOutline (char src, float width) { + // find the cmaps + CmapTable cmap = (CmapTable) this.font.getTable ("cmap"); + + // if there are no cmaps, this is (hopefully) a cid-mapped font, + // so just trust the value we were given for src + if (cmap == null) { + return getOutline ((int) src, width); + } + + CMap[] maps = cmap.getCMaps (); + + // try the maps in order + for (int i = 0; i < maps.length; i++) { + int idx = maps[i].map (src); + if (idx != 0) { + return getOutline (idx, width); + } + } + + // windows symbol font CMap may use one of the following code ranges + if (src >= 0 && src <= 0xFF) { + int[] symbolPages = new int[]{0xF000, 0xF100, 0xF200}; + for (int codePage : symbolPages) { + for (int i = 0; i < maps.length; i++) { + int idx = maps[i].map ( (char)(src | codePage)); + if (idx != 0) { + return getOutline (idx, width); + } + } + } + } + + // not found, return the empty glyph + return getOutline (0, width); + } + + /** + * lookup the outline using the (3, 1) cmap, as specified in 32000-1:2008, + * 9.6.6.4, when an Encoding is specified. + * + * @param val + * @param width + * @return GeneralPath + */ + protected synchronized GeneralPath getOutlineFrom31CMap (char val, + float width) { + // find the cmaps + CmapTable cmap = (CmapTable) this.font.getTable ("cmap"); + + if (cmap == null) { + return null; + } + + // find the (3, 1) cmap subtable (Microsoft Unicode) + CMap map = cmap.getCMap ((short) 3, (short) 1); + if (map == null) { + return null; + } + int idx = map.map (val); + if (idx != 0) { + return getOutline (idx, width); + } + + return null; + } + + /** + * Get the outline of a character given the character name + */ + @Override + protected synchronized GeneralPath getOutline (String name, float width) { + int idx; + PostTable post = (PostTable) this.font.getTable ("post"); + if (post != null) { + idx = post.getGlyphNameIndex (name); + if (idx != 0) { + return getOutline (idx, width); + } + } + + Integer res = AdobeGlyphList.getGlyphNameIndex (name); + if (res != null) { + idx = res; + return getOutlineFrom31CMap ((char) idx, width); + } + return null; + } + + /** + * Get the outline of a character given the glyph id + */ + protected synchronized GeneralPath getOutline (int glyphId, float width) { + // find the glyph itself + GlyfTable glyf = (GlyfTable) this.font.getTable ("glyf"); + Glyf g = glyf.getGlyph (glyphId); + + GeneralPath gp = null; + if (g instanceof GlyfSimple) { + gp = renderSimpleGlyph ((GlyfSimple) g); + } else if (g instanceof GlyfCompound) { + gp = renderCompoundGlyph (glyf, (GlyfCompound) g); + } else { + gp = new GeneralPath (); + } + + // calculate the advance + HmtxTable hmtx = (HmtxTable) this.font.getTable ("hmtx"); + float advance = hmtx.getAdvance (glyphId) / this.unitsPerEm; + + // scale the glyph to match the desired advance + float widthfactor = width / advance; + + // the base transform scales the glyph to 1x1 + AffineTransform at = AffineTransform.getScaleInstance(1 / this.unitsPerEm, 1 / this.unitsPerEm); + if (advance != 0) { + at.concatenate(AffineTransform.getScaleInstance(widthfactor, 1)); + } + + gp.transform (at); + + return gp; + } + + /** + * Render a simple glyf + */ + protected GeneralPath renderSimpleGlyph (GlyfSimple g) { + // the current contour + int curContour = 0; + + // the render state + RenderState rs = new RenderState (); + rs.gp = new GeneralPath (); + + for (int i = 0; i < g.getNumPoints (); i++) { + PointRec rec = new PointRec (g, i); + + if (rec.onCurve) { + addOnCurvePoint (rec, rs); + } else { + addOffCurvePoint (rec, rs); + } + + // see if we just ended a contour + if (i == g.getContourEndPoint (curContour)) { + curContour++; + + if (rs.firstOff != null) { + addOffCurvePoint (rs.firstOff, rs); + } + + if (rs.firstOn != null) { + addOnCurvePoint (rs.firstOn, rs); + } + + rs.firstOn = null; + rs.firstOff = null; + rs.prevOff = null; + } + } + + return rs.gp; + } + + /** + * Render a compound glyf + */ + protected GeneralPath renderCompoundGlyph (GlyfTable glyf, GlyfCompound g) { + GeneralPath gp = new GeneralPath (); + + for (int i = 0; i < g.getNumComponents (); i++) { + // find and render the component glyf + Glyf gl = glyf.getGlyph (g.getGlyphIndex (i)); + GeneralPath path = null; + if (gl instanceof GlyfSimple) { + path = renderSimpleGlyph ((GlyfSimple) gl); + } else if (gl instanceof GlyfCompound) { + path = renderCompoundGlyph (glyf, (GlyfCompound) gl); + } else { + throw new RuntimeException ( + "Unsupported glyph type " + gl.getClass ().getCanonicalName ()); + } + + // multiply the translations by units per em + double[] matrix = g.getTransform (i); + + // transform the path + path.transform (new AffineTransform (matrix)); + + // add it to the global path + gp.append (path, false); + } + + return gp; + } + + /** add a point on the curve */ + private void addOnCurvePoint (PointRec rec, RenderState rs) { + // if the point is on the curve, either move to it, + // or draw a line from the previous point + if (rs.firstOn == null) { + rs.firstOn = rec; + rs.gp.moveTo (rec.x, rec.y); + } else if (rs.prevOff != null) { + rs.gp.quadTo (rs.prevOff.x, rs.prevOff.y, rec.x, rec.y); + rs.prevOff = null; + } else { + rs.gp.lineTo (rec.x, rec.y); + } + } + + /** add a point off the curve */ + private void addOffCurvePoint (PointRec rec, RenderState rs) { + if (rs.prevOff != null) { + PointRec oc = new PointRec ((rec.x + rs.prevOff.x) / 2, + (rec.y + rs.prevOff.y) / 2, + true); + addOnCurvePoint (oc, rs); + } else if (rs.firstOn == null) { + rs.firstOff = rec; + } + rs.prevOff = rec; + } + + class RenderState { + // the shape itself + + GeneralPath gp; + // the first off and on-curve points in the current segment + + PointRec firstOn; + + PointRec firstOff; + // the previous off and on-curve points in the current segment + + PointRec prevOff; + + } + + /** a point on the stack of points */ + static class PointRec { + + int x; + + int y; + + boolean onCurve; + + public PointRec (int x, int y, boolean onCurve) { + this.x = x; + this.y = y; + this.onCurve = onCurve; + } + + public PointRec (GlyfSimple g, int idx) { + this.x = g.getXCoord (idx); + this.y = g.getYCoord (idx); + this.onCurve = g.onCurve (idx); + } + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/Type0Font.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/Type0Font.java new file mode 100644 index 0000000000..667e4af559 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/Type0Font.java @@ -0,0 +1,71 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.font; + +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.PDFObject; + +/** + * Type 0 fonts are composite fonts with a CMAP to map between + * source character codes and destination fonts/codes + * + * @author Jonathan Kaplan + */ +public class Type0Font extends PDFFont { + + /** + * The decendant fonts, indexed by font number from the CMAP + */ + PDFFont[] fonts; + + /** Creates a new instance of Type0Font */ + public Type0Font(String baseFont, PDFObject fontObj, + PDFFontDescriptor descriptor) throws IOException { + super (baseFont, descriptor); + + PDFObject[] descendantFonts = fontObj.getDictRef("DescendantFonts").getArray(); + + this.fonts = new PDFFont[descendantFonts.length]; + + for (int i = 0; i < descendantFonts.length; i++) { + PDFFont descFont = getFont(descendantFonts[i], null); + if (descFont instanceof CIDFontType0) { + ((CIDFontType0)descFont).parseToUnicodeMap(fontObj); + } + this.fonts[i] = descFont; + } + } + + /** + * Get a descendant font of this font by fontId + */ + public PDFFont getDescendantFont(int fontID) { + return this.fonts[fontID]; + } + + /** + * Get a character from the first font in the descendant fonts array + */ + @Override + protected PDFGlyph getGlyph(char src, String name) { + return (getDescendantFont(0).getGlyph(src, name)); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/Type1CFont.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/Type1CFont.java new file mode 100644 index 0000000000..048b3f761f --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/Type1CFont.java @@ -0,0 +1,1191 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.font; + +import java.awt.geom.AffineTransform; +import java.awt.geom.GeneralPath; +import java.awt.geom.NoninvertibleTransformException; +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.PDFDebugger; +import com.github.librepdf.pdfrenderer.PDFObject; + +/** + * A representation, with parser, of an Adobe Type 1C font. + * You can find information about CFF and Type 1C font encoding + * in + * http://partners.adobe.com/public/developer/en/font/5176.CFF.pdf + * and + * http://partners.adobe.com/public/developer/en/font/5177.Type2.pdf + * + * @author Mike Wessler + */ +public class Type1CFont extends OutlineFont { + + String chr2name[] = new String[256]; + + byte[] data; + + int pos; + + byte[] subrs; + + float[] stack = new float[100]; + + int stackptr = 0; + + int stemhints = 0; + + String names[]; + + int glyphnames[]; + + int encoding[] = new int[256]; + + String fontname; + + AffineTransform at = new AffineTransform (0.001f, 0, 0, 0.001f, 0, 0); + + int num; + + float fnum; + + int type; + + static int CMD = 0; + + static int NUM = 1; + + static int FLT = 2; + + /** + * create a new Type1CFont based on a font data stream and a descriptor + * @param baseFont the postscript name of this font + * @param src a stream containing the font + * @param descriptor the descriptor for this font + */ + public Type1CFont (String baseFont, PDFObject src, + PDFFontDescriptor descriptor) throws IOException { + super (baseFont, src, descriptor); + + PDFObject dataObj = descriptor.getFontFile3 (); + this.data = dataObj.getStream (); + this.pos = 0; + parse (); + + // TODO: free up (set to null) unused structures (data, subrs, stack) + } + + /** + * a debug method for printing the data + */ + private void printData () { + char[] parts = new char[17]; + int partsloc = 0; + for (int i = 0; i < this.data.length; i++) { + int d = (this.data[i]) & 0xff; + if (d == 0) { + parts[partsloc++] = '.'; + } else if (d < 32 || d >= 127) { + parts[partsloc++] = '?'; + } else { + parts[partsloc++] = (char) d; + } + if (d < 16) { + PDFDebugger.debug("0" + Integer.toHexString (d), 200); + } else { + PDFDebugger.debug(Integer.toHexString (d), 200); + } + if ((i & 15) == 15) { + PDFDebugger.debug(" " + new String (parts), 200); + partsloc = 0; + } else if ((i & 7) == 7) { + PDFDebugger.debug(" ", 200); + parts[partsloc++] = ' '; + } else if ((i & 1) == 1) { + PDFDebugger.debug(" ", 200); + } + } + } + + /** + * read the next decoded value from the stream + * @param charstring ???? + */ + private int readNext (boolean charstring) { + this.num = (this.data[this.pos++]) & 0xff; + if (this.num == 30 && !charstring) { // goofy floatingpoint rep + readFNum (); + return this.type = FLT; + } else if (this.num == 28) { + this.num = ((this.data[this.pos]) << 8) + ((this.data[this.pos + 1]) & 0xff); + this.pos += 2; + return this.type = NUM; + } else if (this.num == 29 && !charstring) { + this.num = ((this.data[this.pos] & 0xff) << 24) | + ((this.data[this.pos + 1] & 0xff) << 16) | + ((this.data[this.pos + 2] & 0xff) << 8) | + ((this.data[this.pos + 3] & 0xff)); + this.pos += 4; + return this.type = NUM; + } else if (this.num == 12) { // two-byte command + this.num = 1000 + ((this.data[this.pos++]) & 0xff); + return this.type = CMD; + } else if (this.num < 32) { + return this.type = CMD; + } else if (this.num < 247) { + this.num -= 139; + return this.type = NUM; + } else if (this.num < 251) { + this.num = (this.num - 247) * 256 + ((this.data[this.pos++]) & 0xff) + 108; + return this.type = NUM; + } else if (this.num < 255) { + this.num = -(this.num - 251) * 256 - ((this.data[this.pos++]) & 0xff) - 108; + return this.type = NUM; + } else if (!charstring) { // dict shouldn't have a 255 code + printData (); + throw new RuntimeException ("Got a 255 code while reading dict"); + } else { // num was 255 + this.fnum = (((this.data[this.pos] & 0xff) << 24) | + ((this.data[this.pos + 1] & 0xff) << 16) | + ((this.data[this.pos + 2] & 0xff) << 8) | + ((this.data[this.pos + 3] & 0xff))) / 65536f; + this.pos += 4; + return this.type = FLT; + } + } + + /** + * read the next funky floating point number from the input stream. + * value gets put into the fnum field. + */ + public void readFNum () { + // work in nybbles: 0-9=0-9, a=. b=E, c=E-, d=rsvd e=neg f=end + float f = 0; + boolean neg = false; + int exp = 0; + int eval = 0; + float mul = 1; + byte work = this.data[this.pos++]; + while (true) { + if (work == (byte) 0xdd) { + work = this.data[this.pos++]; + } + int nyb = (work >> 4) & 0xf; + work = (byte) ((work << 4) | 0xd); + if (nyb < 10) { + if (exp != 0) { // working on the exponent + eval = eval * 10 + nyb; + } else if (mul == 1) { // working on an int + f = f * 10 + nyb; + } else { // working on decimal part + f += nyb * mul; + mul /= 10f; + } + } else if (nyb == 0xa) { // decimal + mul = 0.1f; + } else if (nyb == 0xb) { // E+ + exp = 1; + } else if (nyb == 0xc) { // E- + exp = -1; + } else if (nyb == 0xe) { // neg + neg = true; + } else { + break; + } + } + this.fnum = (neg ? -1 : 1) * f * (float) Math.pow (10, eval * exp); + } + + /** + * read an integer from the input stream + * @param len the number of bytes in the integer + * @return the integer + */ + private int readInt (int len) { + int n = 0; + for (int i = 0; i < len; i++) { + n = (n << 8) | ((this.data[this.pos++]) & 0xff); + } + return n; + } + + /** + * read the next byte from the stream + * @return the byte + */ + private int readByte () { + return (this.data[this.pos++]) & 0xff; + } + + // DICT structure: + // operand operator operand operator ... + // INDEX structure: + // count(2) offsize [offset offset ... offset] data + // offset array has count+1 entries + // data starts at 3+(count+1)*offsize + // offset for data is offset+2+(count+1)*offsize + /** + * get the size of the dictionary located within the stream at + * some offset. + * @param loc the index of the start of the dictionary + * @return the size of the dictionary, in bytes. + */ + public int getIndexSize (int loc) { + int hold = this.pos; + this.pos = loc; + int count = readInt (2); + if (count <= 0) { + return 2; + } + int encsz = readByte (); + if (encsz < 1 || encsz > 4) { + throw new RuntimeException ("Offsize: " + encsz + + ", must be in range 1-4."); + } + // pos is now at the first offset. last offset is at count*encsz + this.pos += count * encsz; + int end = readInt (encsz); + this.pos = hold; + return 2 + (count + 1) * encsz + end; + } + + /** + * return the number of entries in an Index table. + * + * @param loc + * @return + */ + public int getTableLength (int loc) { + int hold = this.pos; + this.pos = loc; + int count = readInt (2); + if (count <= 0) { + return 2; + } + this.pos = hold; + return count; + } + + /** + * A range. There's probably a version of this class floating around + * somewhere already in Java. + */ + static class Range { + + private final int start; + + private final int len; + + public Range (int start, int len) { + this.start = start; + this.len = len; + } + + public final int getStart () { + return this.start; + } + + public final int getLen () { + return this.len; + } + + public final int getEnd () { + return this.start + this.len; + } + + @Override + public String toString () { + return "Range: start: " + this.start + ", len: " + this.len; + } + } + + /** + * Get the range of a particular index in a dictionary. + * @param index the start of the dictionary. + * @param id the index of the entry in the dictionary + * @return a range describing the offsets of the start and end of + * the entry from the start of the file, not the dictionary + */ + Range getIndexEntry (int index, int id) { + int hold = this.pos; + this.pos = index; + int count = readInt (2); + int encsz = readByte (); + if (encsz < 1 || encsz > 4) { + throw new RuntimeException ("Offsize: " + encsz + + ", must be in range 1-4."); + } + this.pos += encsz * id; + int from = readInt (encsz); + Range r = new Range (from + 2 + index + encsz * (count + 1), readInt ( + encsz) - from); + this.pos = hold; + return r; + } + // Top DICT: NAME CODE DEFAULT + // charstringtype 12 6 2 + // fontmatrix 12 7 0.001 0 0 0.001 + // charset 15 - (offset) names of glyphs (ref to name idx) + // encoding 16 - (offset) array of codes + // CharStrings 17 - (offset) + // Private 18 - (size, offset) + // glyph at position i in CharStrings has name charset[i] + // and code encoding[i] + int charstringtype = 2; + + float temps[] = new float[32]; + + int charsetbase = 0; + + int encodingbase = 0; + + int charstringbase = 0; + + int privatebase = 0; + + int privatesize = 0; + + int gsubrbase = 0; + + int lsubrbase = 0; + + int gsubrsoffset = 0; + + int lsubrsoffset = 0; + + int nglyphs = 1; + + /** + * read a dictionary that exists within some range, parsing the entries + * within the dictionary. + */ + private void readDict (Range r) { + this.pos = r.getStart (); + while (this.pos < r.getEnd ()) { + int cmd = readCommand (false); + if (cmd == 1006) { // charstringtype, default=2 + this.charstringtype = (int) this.stack[0]; + } else if (cmd == 1007) { // fontmatrix + if (this.stackptr == 4) { + this.at = new AffineTransform (this.stack[0], this.stack[1], + this.stack[2], this.stack[3], + 0, 0); + } else { + this.at = new AffineTransform (this.stack[0], this.stack[1], + this.stack[2], this.stack[3], + this.stack[4], this.stack[5]); + } + } else if (cmd == 15) { // charset + this.charsetbase = (int) this.stack[0]; + } else if (cmd == 16) { // encoding + this.encodingbase = (int) this.stack[0]; + } else if (cmd == 17) { // charstrings + this.charstringbase = (int) this.stack[0]; + } else if (cmd == 18) { // private + this.privatesize = (int) this.stack[0]; + this.privatebase = (int) this.stack[1]; + } else if (cmd == 19) { // subrs (in Private dict) + this.lsubrbase = this.privatebase + (int) this.stack[0]; + this.lsubrsoffset = calcoffset (this.lsubrbase); + } + this.stackptr = 0; + } + } + + /** + * read a complete command. this may involve several numbers + * which go onto a stack before an actual command is read. + * @param charstring ???? + * @return the command. Some numbers may also be on the stack. + */ + private int readCommand (boolean charstring) { + while (true) { + int t = readNext (charstring); + if (t == CMD) { + return this.num; + } else { + this.stack[this.stackptr++] = (t == NUM) ? (float) this.num : this.fnum; + } + } + } + + /** + * parse information about the encoding of this file. + * @param base the start of the encoding data + */ + private void readEncodingData (int base) { + if (base == 0) { // this is the StandardEncoding + System.arraycopy (FontSupport.standardEncoding, 0, this.encoding, 0, + FontSupport.standardEncoding.length); + } else if (base == 1) { // this is the expert encoding + PDFDebugger.debug("**** EXPERT ENCODING not yet implemented!"); + // TODO: copy ExpertEncoding + } else { + this.pos = base; + int encodingtype = readByte (); + if ((encodingtype & 127) == 0) { + int ncodes = readByte (); + for (int i = 1; i < ncodes + 1; i++) { + int idx = readByte () & 0xff; + this.encoding[idx] = i; + } + } else if ((encodingtype & 127) == 1) { + int nranges = readByte (); + int p = 1; + for (int i = 0; i < nranges; i++) { + int start = readByte (); + int more = readByte (); + for (int j = start; j < start + more + 1; j++) { + this.encoding[j] = p++; + } + } + } else { + PDFDebugger.debug("Bad encoding type: " + encodingtype); + } + // TODO: now check for supplemental encoding data + } + } + + /** + * read the names of the glyphs. + * @param base the start of the glyph name table + */ + private void readGlyphNames (int base) { + if (base == 0) { + this.glyphnames = new int[229]; + for (int i = 0; i < this.glyphnames.length; i++) { + this.glyphnames[i] = i; + } + return; + } else if (base == 1) { + this.glyphnames = FontSupport.type1CExpertCharset; + return; + } else if (base == 2) { + this.glyphnames = FontSupport.type1CExpertSubCharset; + return; + } + // nglyphs has already been set. + this.glyphnames = new int[this.nglyphs]; + this.glyphnames[0] = 0; + this.pos = base; + int t = readByte (); + if (t == 0) { + for (int i = 1; i < this.nglyphs; i++) { + this.glyphnames[i] = readInt (2); + } + } else if (t == 1) { + int n = 1; + while (n < this.nglyphs) { + int sid = readInt (2); + int range = readByte () + 1; + for (int i = 0; i < range; i++) { + this.glyphnames[n++] = sid++; + } + } + } else if (t == 2) { + int n = 1; + while (n < this.nglyphs) { + int sid = readInt (2); + int range = readInt (2) + 1; + for (int i = 0; i < range; i++) { + this.glyphnames[n++] = sid++; + } + } + } + } + + /** + * read a list of names + * @param base the start of the name table + */ + private void readNames (int base) { + this.pos = base; + int nextra = readInt (2); + this.names = new String[nextra]; + // safenames= new String[nextra]; + for (int i = 0; i < nextra; i++) { + Range r = getIndexEntry (base, i); + this.names[i] = new String (this.data, r.getStart (), r.getLen ()); + PDFDebugger.debug("Read name: "+i+" from "+r.getStart()+" to "+r.getEnd()+": "+safe(names[i]), 1000); + } + } + + /** + * parse the font data. + * @param encdif a dictionary describing the encoding. + */ + private void parse () throws IOException { + int majorVersion = readByte (); + int minorVersion = readByte (); + int hdrsz = readByte (); + int offsize = readByte (); + // jump over rest of header: base of font names index + int fnames = hdrsz; + // offset in the file of the array of font dicts + int topdicts = fnames + getIndexSize (fnames); + // offset in the file of local names + int theNames = topdicts + getIndexSize (topdicts); + // offset in the file of the array of global subroutines + this.gsubrbase = theNames + getIndexSize (theNames); + this.gsubrsoffset = calcoffset (this.gsubrbase); + // read extra names + readNames (theNames); + // does this file have more than one font? + this.pos = topdicts; + if (readInt (2) != 1) { + printData (); + throw new RuntimeException ("More than one font in this file!"); + } + Range r = getIndexEntry (fnames, 0); + this.fontname = new String (this.data, r.getStart (), r.getLen ()); + // read first dict + readDict (getIndexEntry (topdicts, 0)); + // read the private dictionary + readDict (new Range (this.privatebase, this.privatesize)); + // calculate the number of glyphs + this.pos = this.charstringbase; + this.nglyphs = readInt (2); + // now get the glyph names + readGlyphNames (this.charsetbase); + // now figure out the encoding + readEncodingData (this.encodingbase); + } + + /** + * get the index of a particular name. The name table starts with + * the standard names in FontSupport.stdNames, and is appended by + * any names in the name table from this font's dictionary. + */ + private int getNameIndex (String name) { + int val = FontSupport.findName (name, FontSupport.stdNames); + if (val == -1) { + val = FontSupport.findName (name, this.names) + FontSupport.stdNames.length; + } + if (val == -1) { + val = 0; + } + return val; + } + + /** + * convert a string to one in which any non-printable bytes are + * replaced by "<###>" where ## is the value of the byte. + */ + private String safe (String src) { + StringBuffer sb = new StringBuffer (); + for (int i = 0; i < src.length (); i++) { + char c = src.charAt (i); + if (c >= 32 && c < 128) { + sb.append (c); + } else { + sb.append ("<" + (int) c + ">"); + } + } + return sb.toString (); + } + + /** + * Read the data for a glyph from the glyph table, and transform + * it based on the current transform. + * + * @param base the start of the glyph table + * @param offset the index of this glyph in the glyph table + */ + private synchronized GeneralPath readGlyph (int base, int offset) { + FlPoint pt = new FlPoint (); + + // find this entry + Range r = getIndexEntry (base, offset); + + // create a path + GeneralPath gp = new GeneralPath (); + + + // rember the start position (for recursive calls due to seac) + int hold = this.pos; + + // read the glyph itself + this.stackptr = 0; + this.stemhints = 0; + parseGlyph (r, gp, pt); + + // restore the start position + this.pos = hold; + + gp.transform (this.at); + + return gp; + } + + /** + * calculate an offset code for a dictionary. Uses the count of entries + * to determine what the offset should be. + * + * @param base the index of the start of the dictionary + */ + public int calcoffset (int base) { + int len = getTableLength (base); + if (len < 1240) { + return 107; + } else if (len < 33900) { + return 1131; + } else { + return 32768; + } + } + + /** + * get the name associated with an ID. + * @param id the index of the name + * @return the name from the FontSupport.stdNames table augmented + * by the local name table + */ + public String getSID (int id) { + if (id < FontSupport.stdNames.length) { + return FontSupport.stdNames[id]; + } else { + id -= FontSupport.stdNames.length; + return this.names[id]; + } + } + + /** + * build an accented character out of two pre-defined glyphs. + * @param x the x offset of the accent + * @param y the y offset of the accent + * @param b the index of the base glyph + * @param a the index of the accent glyph + * @param gp the GeneralPath into which the combined glyph will be + * written. + */ + private void buildAccentChar (float x, float y, char b, char a, + GeneralPath gp) { + // get the outline of the accent + GeneralPath pathA = getOutline (a, getWidth (a, null)); + + // undo the effect of the transform applied in read + AffineTransform xformA = AffineTransform.getTranslateInstance (x, y); + try { + xformA.concatenate (this.at.createInverse ()); + } catch (NoninvertibleTransformException nte) { + // oh well ... + } + pathA.transform (xformA); + + GeneralPath pathB = getOutline (b, getWidth (b, null)); + + try { + AffineTransform xformB = this.at.createInverse (); + pathB.transform (xformB); + } catch (NoninvertibleTransformException nte) { + // ignore + } + + gp.append (pathB, false); + gp.append (pathA, false); + } + + /** + * parse a glyph defined in a particular range + * @param r the range of the glyph definition + * @param gp a GeneralPath in which to store the glyph outline + * @param pt a FlPoint representing the end of the current path + */ + void parseGlyph (Range r, GeneralPath gp, FlPoint pt) { + this.pos = r.getStart (); + int i; + float x1, y1, x2, y2, x3, y3, ybase; + int hold; + while (this.pos < r.getEnd ()) { + int cmd = readCommand (true); + hold = 0; + switch (cmd) { + case 1: // hstem + case 3: // vstem + this.stackptr = 0; + break; + case 4: // vmoveto + if (this.stackptr > 1) { // this is the first call, arg1 is width + this.stack[0] = this.stack[1]; + } + pt.y += this.stack[0]; + if (pt.open) { + gp.closePath (); + } + pt.open = false; + gp.moveTo (pt.x, pt.y); + this.stackptr = 0; + break; + case 5: // rlineto + for (i = 0; i < this.stackptr;) { + pt.x += this.stack[i++]; + pt.y += this.stack[i++]; + gp.lineTo (pt.x, pt.y); + } + pt.open = true; + this.stackptr = 0; + break; + case 6: // hlineto + for (i = 0; i < this.stackptr;) { + if ((i & 1) == 0) { + pt.x += this.stack[i++]; + } else { + pt.y += this.stack[i++]; + } + gp.lineTo (pt.x, pt.y); + } + pt.open = true; + this.stackptr = 0; + break; + case 7: // vlineto + for (i = 0; i < this.stackptr;) { + if ((i & 1) == 0) { + pt.y += this.stack[i++]; + } else { + pt.x += this.stack[i++]; + } + gp.lineTo (pt.x, pt.y); + } + pt.open = true; + this.stackptr = 0; + break; + case 8: // rrcurveto + for (i = 0; i < this.stackptr;) { + x1 = pt.x + this.stack[i++]; + y1 = pt.y + this.stack[i++]; + x2 = x1 + this.stack[i++]; + y2 = y1 + this.stack[i++]; + pt.x = x2 + this.stack[i++]; + pt.y = y2 + this.stack[i++]; + gp.curveTo (x1, y1, x2, y2, pt.x, pt.y); + } + pt.open = true; + this.stackptr = 0; + break; + case 10: // callsubr + hold = this.pos; + i = (int) this.stack[--this.stackptr] + this.lsubrsoffset; + Range lsubr = getIndexEntry (this.lsubrbase, i); + parseGlyph (lsubr, gp, pt); + this.pos = hold; + break; + case 11: // return + return; + case 14: // endchar + // width x y achar bchar endchar == x y achar bchar seac + if (this.stackptr == 5) { + buildAccentChar (this.stack[1], this.stack[2], (char) this.stack[3], + (char) this.stack[4], gp); + } else if (this.stackptr == 4) { + // see page 58 on specification 5177.Type2.pdf which indicates that + // these parameters are valid for Type1C as the width is optional + buildAccentChar(this.stack[0], this.stack[1], (char) this.stack[2], (char) this.stack[3], gp); + } + if (pt.open) { + gp.closePath (); + } + pt.open = false; + this.stackptr = 0; + stemhints = 0; + break; + case 18: // hstemhm + stemhints += (this.stackptr) / 2; + this.stackptr = 0; + break; + case 19: // hintmask + case 20: // cntrmask + stemhints += (this.stackptr) / 2; + this.pos += (stemhints - 1) / 8 + 1; + this.stackptr = 0; + break; + case 21: // rmoveto + if (this.stackptr > 2) { + this.stack[0] = this.stack[1]; + this.stack[1] = this.stack[2]; + } + pt.x += this.stack[0]; + pt.y += this.stack[1]; + if (pt.open) { + gp.closePath (); + } + gp.moveTo (pt.x, pt.y); + pt.open = false; + this.stackptr = 0; + break; + case 22: // hmoveto + if (this.stackptr > 1) { + this.stack[0] = this.stack[1]; + } + pt.x += this.stack[0]; + if (pt.open) { + gp.closePath (); + } + gp.moveTo (pt.x, pt.y); + pt.open = false; + this.stackptr = 0; + break; + case 23: // vstemhm + stemhints += (this.stackptr) / 2; + this.stackptr = 0; + break; + case 24: // rcurveline + for (i = 0; i < this.stackptr - 2;) { + x1 = pt.x + this.stack[i++]; + y1 = pt.y + this.stack[i++]; + x2 = x1 + this.stack[i++]; + y2 = y1 + this.stack[i++]; + pt.x = x2 + this.stack[i++]; + pt.y = y2 + this.stack[i++]; + gp.curveTo (x1, y1, x2, y2, pt.x, pt.y); + } + pt.x += this.stack[i++]; + pt.y += this.stack[i++]; + gp.lineTo (pt.x, pt.y); + pt.open = true; + this.stackptr = 0; + break; + case 25: // rlinecurve + for (i = 0; i < this.stackptr - 6;) { + pt.x += this.stack[i++]; + pt.y += this.stack[i++]; + gp.lineTo (pt.x, pt.y); + } + x1 = pt.x + this.stack[i++]; + y1 = pt.y + this.stack[i++]; + x2 = x1 + this.stack[i++]; + y2 = y1 + this.stack[i++]; + pt.x = x2 + this.stack[i++]; + pt.y = y2 + this.stack[i++]; + gp.curveTo (x1, y1, x2, y2, pt.x, pt.y); + pt.open = true; + this.stackptr = 0; + break; + case 26: // vvcurveto + i = 0; + if ((this.stackptr & 1) == 1) { // odd number of arguments + pt.x += this.stack[i++]; + } + while (i < this.stackptr) { + x1 = pt.x; + y1 = pt.y + this.stack[i++]; + x2 = x1 + this.stack[i++]; + y2 = y1 + this.stack[i++]; + pt.x = x2; + pt.y = y2 + this.stack[i++]; + gp.curveTo (x1, y1, x2, y2, pt.x, pt.y); + } + pt.open = true; + this.stackptr = 0; + break; + case 27: // hhcurveto + i = 0; + if ((this.stackptr & 1) == 1) { // odd number of arguments + pt.y += this.stack[i++]; + } + while (i < this.stackptr) { + x1 = pt.x + this.stack[i++]; + y1 = pt.y; + x2 = x1 + this.stack[i++]; + y2 = y1 + this.stack[i++]; + pt.x = x2 + this.stack[i++]; + pt.y = y2; + gp.curveTo (x1, y1, x2, y2, pt.x, pt.y); + } + pt.open = true; + this.stackptr = 0; + break; + case 29: // callgsubr + hold = this.pos; + i = (int) this.stack[--this.stackptr] + this.gsubrsoffset; + Range gsubr = getIndexEntry (this.gsubrbase, i); + parseGlyph (gsubr, gp, pt); + this.pos = hold; + break; + case 30: // vhcurveto + hold = 4; + case 31: // hvcurveto + for (i = 0; i < this.stackptr;) { + boolean hv = (((i + hold) & 4) == 0); + x1 = pt.x + (hv ? this.stack[i++] : 0); + y1 = pt.y + (hv ? 0 : this.stack[i++]); + x2 = x1 + this.stack[i++]; + y2 = y1 + this.stack[i++]; + pt.x = x2 + (hv ? 0 : this.stack[i++]); + pt.y = y2 + (hv ? this.stack[i++] : 0); + if (i == this.stackptr - 1) { + if (hv) { + pt.x += this.stack[i++]; + } else { + pt.y += this.stack[i++]; + } + } + gp.curveTo (x1, y1, x2, y2, pt.x, pt.y); + } + pt.open = true; + this.stackptr = 0; + break; + case 1000: // old dotsection command. ignore. + this.stackptr = 0; + break; + case 1003: // and + x1 = this.stack[--this.stackptr]; + y1 = this.stack[--this.stackptr]; + this.stack[this.stackptr++] = ((x1 != 0) && (y1 != 0)) ? 1 : 0; + break; + case 1004: // or + x1 = this.stack[--this.stackptr]; + y1 = this.stack[--this.stackptr]; + this.stack[this.stackptr++] = ((x1 != 0) || (y1 != 0)) ? 1 : 0; + break; + case 1005: // not + x1 = this.stack[--this.stackptr]; + this.stack[this.stackptr++] = (x1 == 0) ? 1 : 0; + break; + case 1009: // abs + this.stack[this.stackptr - 1] = Math.abs (this.stack[this.stackptr - 1]); + break; + case 1010: // add + x1 = this.stack[--this.stackptr]; + y1 = this.stack[--this.stackptr]; + this.stack[this.stackptr++] = x1 + y1; + break; + case 1011: // sub + x1 = this.stack[--this.stackptr]; + y1 = this.stack[--this.stackptr]; + this.stack[this.stackptr++] = y1 - x1; + break; + case 1012: // div + x1 = this.stack[--this.stackptr]; + y1 = this.stack[--this.stackptr]; + this.stack[this.stackptr++] = y1 / x1; + break; + case 1014: // neg + this.stack[this.stackptr - 1] = -this.stack[this.stackptr - 1]; + break; + case 1015: // eq + x1 = this.stack[--this.stackptr]; + y1 = this.stack[--this.stackptr]; + this.stack[this.stackptr++] = (x1 == y1) ? 1 : 0; + break; + case 1018: // drop + this.stackptr--; + break; + case 1020: // put + i = (int) this.stack[--this.stackptr]; + x1 = this.stack[--this.stackptr]; + this.temps[i] = x1; + break; + case 1021: // get + i = (int) this.stack[--this.stackptr]; + this.stack[this.stackptr++] = this.temps[i]; + break; + case 1022: // ifelse + if (this.stack[this.stackptr - 2] > this.stack[this.stackptr - 1]) { + this.stack[this.stackptr - 4] = this.stack[this.stackptr - 3]; + } + this.stackptr -= 3; + break; + case 1023: // random + this.stack[this.stackptr++] = (float) Math.random (); + break; + case 1024: // mul + x1 = this.stack[--this.stackptr]; + y1 = this.stack[--this.stackptr]; + this.stack[this.stackptr++] = y1 * x1; + break; + case 1026: // sqrt + this.stack[this.stackptr - 1] = (float) Math.sqrt (this.stack[this.stackptr - 1]); + break; + case 1027: // dup + x1 = this.stack[this.stackptr - 1]; + this.stack[this.stackptr++] = x1; + break; + case 1028: // exch + x1 = this.stack[this.stackptr - 1]; + this.stack[this.stackptr - 1] = this.stack[this.stackptr - 2]; + this.stack[this.stackptr - 2] = x1; + break; + case 1029: // index + i = (int) this.stack[this.stackptr - 1]; + if (i < 0) { + i = 0; + } + this.stack[this.stackptr - 1] = this.stack[this.stackptr - 2 - i]; + break; + case 1030: // roll + i = (int) this.stack[--this.stackptr]; + int n = (int) this.stack[--this.stackptr]; + // roll n number by i (+ = upward) + if (i > 0) { + i = i % n; + } else { + i = n - (-i % n); + } + // x x x x i y y y -> y y y x x x x i (where i=3) + if (i > 0) { + float roll[] = new float[n]; + System.arraycopy (this.stack, this.stackptr - 1 - i, roll, 0, i); + System.arraycopy (this.stack, this.stackptr - 1 - n, roll, i, + n - i); + System.arraycopy (roll, 0, this.stack, this.stackptr - 1 - n, n); + } + break; + case 1034: // hflex + x1 = pt.x + this.stack[0]; + y1 = ybase = pt.y; + x2 = x1 + this.stack[1]; + y2 = y1 + this.stack[2]; + pt.x = x2 + this.stack[3]; + pt.y = y2; + gp.curveTo (x1, y1, x2, y2, pt.x, pt.y); + x1 = pt.x + this.stack[4]; + y1 = pt.y; + x2 = x1 + this.stack[5]; + y2 = ybase; + pt.x = x2 + this.stack[6]; + pt.y = y2; + gp.curveTo (x1, y1, x2, y2, pt.x, pt.y); + pt.open = true; + this.stackptr = 0; + break; + case 1035: // flex + x1 = pt.x + this.stack[0]; + y1 = pt.y + this.stack[1]; + x2 = x1 + this.stack[2]; + y2 = y1 + this.stack[3]; + pt.x = x2 + this.stack[4]; + pt.y = y2 + this.stack[5]; + gp.curveTo (x1, y1, x2, y2, pt.x, pt.y); + x1 = pt.x + this.stack[6]; + y1 = pt.y + this.stack[7]; + x2 = x1 + this.stack[8]; + y2 = y1 + this.stack[9]; + pt.x = x2 + this.stack[10]; + pt.y = y2 + this.stack[11]; + gp.curveTo (x1, y1, x2, y2, pt.x, pt.y); + pt.open = true; + this.stackptr = 0; + break; + case 1036: // hflex1 + ybase = pt.y; + x1 = pt.x + this.stack[0]; + y1 = pt.y + this.stack[1]; + x2 = x1 + this.stack[2]; + y2 = y1 + this.stack[3]; + pt.x = x2 + this.stack[4]; + pt.y = y2; + gp.curveTo (x1, y1, x2, y2, pt.x, pt.y); + x1 = pt.x + this.stack[5]; + y1 = pt.y; + x2 = x1 + this.stack[6]; + y2 = y1 + this.stack[7]; + pt.x = x2 + this.stack[8]; + pt.y = ybase; + gp.curveTo (x1, y1, x2, y2, pt.x, pt.y); + pt.open = true; + this.stackptr = 0; + break; + case 1037: // flex1 + ybase = pt.y; + float xbase = pt.x; + x1 = pt.x + this.stack[0]; + y1 = pt.y + this.stack[1]; + x2 = x1 + this.stack[2]; + y2 = y1 + this.stack[3]; + pt.x = x2 + this.stack[4]; + pt.y = y2 + this.stack[5]; + gp.curveTo (x1, y1, x2, y2, pt.x, pt.y); + x1 = pt.x + this.stack[6]; + y1 = pt.y + this.stack[7]; + x2 = x1 + this.stack[8]; + y2 = y1 + this.stack[9]; + if (Math.abs (x2 - xbase) > Math.abs (y2 - ybase)) { + pt.x = x2 + this.stack[10]; + pt.y = ybase; + } else { + pt.x = xbase; + pt.y = y2 + this.stack[10]; + } + gp.curveTo (x1, y1, x2, y2, pt.x, pt.y); + pt.open = true; + this.stackptr = 0; + break; + default: + PDFDebugger.debug("ERROR! TYPE1C CHARSTRING CMD IS " + cmd); + break; + } + } + } + + /** + * Get a glyph outline by name + * + * @param name the name of the desired glyph + * @return the glyph outline, or null if unavailable + */ + @Override + protected GeneralPath getOutline (String name, float width) { + // first find the index of this name + int index = getNameIndex (name); + + // now find the glyph with that name + for (int i = 0; i < this.glyphnames.length; i++) { + if (this.glyphnames[i] == index) { + return readGlyph (this.charstringbase, i); + } + } + + // not found -- return the unknown glyph + return readGlyph (this.charstringbase, 0); + } + + /** + * Get a glyph outline by character code + * + * Note this method must always return an outline + * + * @param src the character code of the desired glyph + * @return the glyph outline + */ + @Override + protected GeneralPath getOutline (char src, float width) { + // ignore high bits + int index = (src & 0xff); + + // if we use a standard encoding, the mapping is from glyph to SID + // therefore we must find the glyph index in the name table + if (this.encodingbase == 0 || this.encodingbase == 1) { + for (int i = 0; i < this.glyphnames.length; i++) { + if (this.glyphnames[i] == this.encoding[index]) { + return readGlyph (this.charstringbase, i); + } + } + } else { + // for a custom encoding, the mapping is from glyph to GID, so + // we can just map the glyph directly + if (index > 0 && index < this.encoding.length) { + return readGlyph (this.charstringbase, this.encoding[index]); + } + } + + // for some reason the glyph was not found, return the empty glyph + return readGlyph (this.charstringbase, 0); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/Type1Font.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/Type1Font.java new file mode 100644 index 0000000000..91d166c441 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/Type1Font.java @@ -0,0 +1,907 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.font; + +import java.awt.geom.AffineTransform; +import java.awt.geom.GeneralPath; +import java.awt.geom.NoninvertibleTransformException; +import java.awt.geom.Point2D; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +import com.github.librepdf.pdfrenderer.PDFDebugger; +import com.github.librepdf.pdfrenderer.PDFFile; +import com.github.librepdf.pdfrenderer.PDFObject; + +/** + * A representation, with parser, of an Adobe Type 1 font. + * @author Mike Wessler + */ +public class Type1Font extends OutlineFont { + + String chr2name[]; + int password; + byte[] subrs[]; + int lenIV; + Map name2outline; + Map name2width; + AffineTransform at; + /** the Type1 stack of command values */ + float stack[] = new float[100]; + /** the current position in the Type1 stack */ + int sloc = 0; + /** the stack of postscript commands (used by callothersubr) */ + float psStack[] = new float[3]; + /** the current position in the postscript stack */ + int psLoc = 0; + + /** + * create a new Type1Font based on a font data stream and an encoding. + * @param baseName the postscript name of this font + * @param src the Font object as a stream with a dictionary + * @param descriptor the descriptor for this font + */ + public Type1Font(String baseName, PDFObject src, + PDFFontDescriptor descriptor) throws IOException { + super(baseName, src, descriptor); + + if (descriptor != null && descriptor.getFontFile() != null) { + // parse that file, filling name2outline and chr2name + int start = descriptor.getFontFile().getDictRef("Length1").getIntValue(); + int len = descriptor.getFontFile().getDictRef("Length2").getIntValue(); + byte font[] = descriptor.getFontFile().getStream(); + + parseFont(font, start, len); + } + } + + /** Read a font from it's data, start position and length */ + protected void parseFont(byte[] font, int start, int len) { + this.name2width = new HashMap(); + + byte data[] = null; + + if (isASCII(font, start)) { + byte[] bData = readASCII(font, start, start + len); + data = decrypt(bData, 0, bData.length, 55665, 4); + } else { + data = decrypt(font, start, start + len, 55665, 4); + } + + // encoding is in cleartext area + this.chr2name = readEncoding(font); + int lenIVLoc = findSlashName(data, "lenIV"); + PSParser psp = new PSParser(data, 0); + if (lenIVLoc < 0) { + this.lenIV = 4; + } else { + psp.setLoc(lenIVLoc + 6); + this.lenIV = Integer.parseInt(psp.readThing()); + } + this.password = 4330; + int matrixloc = findSlashName(font, "FontMatrix"); + if (matrixloc < 0) { + PDFDebugger.debug("No FontMatrix!"); + this.at = new AffineTransform(0.001f, 0, 0, 0.001f, 0, 0); + } else { + PSParser psp2 = new PSParser(font, matrixloc + 11); + // read [num num num num num num] + float xf[] = psp2.readArray(6); + this.at = new AffineTransform(xf); + } + + this.subrs = readSubrs(data); + this.name2outline = new TreeMap(readChars(data)); + // at this point, name2outline holds name -> byte[]. + } + + /** + * parse the encoding portion of the font definition + * @param d the font definition stream + * @return an array of the glyphs corresponding to each byte + */ + private String[] readEncoding(byte[] d) { + byte[][] ary = readArray(d, "Encoding", "def"); + String res[] = new String[256]; + for (int i = 0; i < ary.length; i++) { + if (ary[i] != null) { + if (ary[i][0] == '/') { + res[i] = new String(ary[i]).substring(1); + } else { + res[i] = new String(ary[i]); + } + } else { + res[i] = null; + } + } + return res; + } + + /** + * read the subroutines out of the font definition + * @param d the font definition stream + * @return an array of the subroutines, each as a byte array. + */ + private byte[][] readSubrs(byte[] d) { + return readArray(d, "Subrs", "index"); + } + + /** + * read a named array out of the font definition. + *

    + * this function attempts to parse an array out of a postscript + * definition without doing any postscript. It's actually looking + * for things that look like "dup id elt put", and + * placing the elt at the ith position in the array. + * @param d the font definition stream + * @param key the name of the array + * @param end a string that appears at the end of the array + * @return an array consisting of a byte array for each entry + */ + private byte[][] readArray(byte[] d, String key, String end) { + int i = findSlashName(d, key); + if (i < 0) { + // not found. + return new byte[0][]; + } + // now find things that look like "dup id elt put" + // end at "def" + PSParser psp = new PSParser(d, i); + String type = psp.readThing(); // read the key (i is the start of the key) + double val; + type = psp.readThing(); + if (type.equals("StandardEncoding")) { + byte[] stdenc[] = new byte[FontSupport.standardEncoding.length][]; + for (i = 0; i < stdenc.length; i++) { + stdenc[i] = FontSupport.getName(FontSupport.standardEncoding[i]).getBytes(); + } + return stdenc; + } + int len = Integer.parseInt(type); + byte[] out[] = new byte[len][]; + byte[] line; + while (true) { + String s = psp.readThing(); + if (s.equals("dup")) { + String thing = psp.readThing(); + int id = 0; + try { + id = Integer.parseInt(thing); + } catch (Exception e) { + break; + } + String elt = psp.readThing(); + line = elt.getBytes(); + if (Character.isDigit(elt.charAt(0))) { + int hold = Integer.parseInt(elt); + String special = psp.readThing(); + if (special.equals("-|") || special.equals("RD")) { + psp.setLoc(psp.getLoc() + 1); + line = psp.getNEncodedBytes(hold, this.password, this.lenIV); + } + } + out[id] = line; + } else if (s.equals(end)) { + break; + } + } + return out; + } + + /** + * decrypt an array using the Adobe Type 1 Font decryption algorithm. + * @param d the input array of bytes + * @param start where in the array to start decoding + * @param end where in the array to stop decoding + * @param key the decryption key + * @param skip how many bytes to skip initially + * @return the decrypted bytes. The length of this array will be + * (start-end-skip) bytes long + */ + private byte[] decrypt(byte[] d, int start, int end, int key, int skip) { + if (end - start - skip < 0) { + skip = 0; + } + byte[] o = new byte[end - start - skip]; + int r = key; + int ipos; + int c1 = 52845; + int c2 = 22719; + for (ipos = start; ipos < end; ipos++) { + int c = d[ipos] & 0xff; + int p = (c ^ (r >> 8)) & 0xff; + r = ((c + r) * c1 + c2) & 0xffff; + if (ipos - start - skip >= 0) { + o[ipos - start - skip] = (byte) p; + } + } + return o; + } + + /** + * Read data formatted as ASCII strings as binary data + * + * @param data the data, formatted as ASCII strings + * @param start where in the array to start decrypting + * @param end where in the array to stop decrypting + */ + private byte[] readASCII(byte[] data, int start, int end) { + // each byte of output is derived from one character (two bytes) of + // input + byte[] o = new byte[(end - start) / 2]; + + int count = 0; + int bit = 0; + + for (int loc = start; loc < end; loc++) { + char c = (char) (data[loc] & 0xff); + byte b = (byte) 0; + + if (c >= '0' && c <= '9') { + b = (byte) (c - '0'); + } else if (c >= 'a' && c <= 'f') { + b = (byte) (10 + (c - 'a')); + } else if (c >= 'A' && c <= 'F') { + b = (byte) (10 + (c - 'A')); + } else { + // linefeed or something. Skip. + continue; + } + + // which half of the byte are we? + if ((bit++ % 2) == 0) { + o[count] = (byte) (b << 4); + } else { + o[count++] |= b; + } + } + + return o; + } + + /** + * Determine if data is in ASCII or binary format. According to the spec, + * if any of the first 4 bytes are not character codes ('0' - '9' or + * 'A' - 'F' or 'a' - 'f'), then the data is binary. Otherwise it is + * ASCII + */ + private boolean isASCII(byte[] data, int start) { + // look at the first 4 bytes + for (int i = start; i < start + 4; i++) { + // get the byte as a character + char c = (char) (data[i] & 0xff); + + if (c >= '0' && c <= '9') { + continue; + } else if (c >= 'a' && c <= 'f') { + continue; + } else if (c >= 'A' && c <= 'F') { + continue; + } else { + // out of range + return false; + } + } + + // all were in range, so it is ASCII + return true; + } + + /** + * PostScript reader (not a parser, as the name would seem to indicate). + */ + class PSParser { + + byte[] data; + int loc; + + /** + * create a PostScript reader given some data and an initial offset + * into that data. + * @param data the bytes of the postscript information + * @param start an initial offset into the data + */ + public PSParser(byte[] data, int start) { + this.data = data; + this.loc = start; + } + + /** + * get the next postscript "word". This is basically the next + * non-whitespace block between two whitespace delimiters. + * This means that something like " [2 4 53]" will produce + * three items, while " [2 4 56 ]" will produce four. + */ + public String readThing() { + // skip whitespace + while (PDFFile.isWhiteSpace(this.data[this.loc])) { + this.loc++; + } + // read thing + int start = this.loc; + while (!PDFFile.isWhiteSpace(this.data[this.loc])) { + this.loc++; + if (!PDFFile.isRegularCharacter(this.data[this.loc])) { + break; // leave with the delimiter included + } + } + String s = new String(this.data, start, this.loc - start); + return s; + } + + /** + * read a set of numbers from the input. This method doesn't + * pay any attention to "[" or "]" delimiters, and reads any + * non-numeric items as the number 0. + * @param count the number of items to read + * @return an array of count floats + */ + public float[] readArray(int count) { + float[] ary = new float[count]; + int idx = 0; + while (idx < count) { + String thing = readThing(); + if (thing.charAt(0) == '[') { + thing = thing.substring(1); + } + if (thing.endsWith("]")) { + thing = thing.substring(0, thing.length() - 1); + } + if (thing.length() > 0) { + ary[idx++] = Float.parseFloat(thing); + } + } + return ary; + } + + /** + * get the current location within the input stream + */ + public int getLoc() { + return this.loc; + } + + /** + * set the current location within the input stream + */ + public void setLoc(int loc) { + this.loc = loc; + } + + /** + * treat the next n bytes of the input stream as encoded + * information to be decrypted. + * @param n the number of bytes to decrypt + * @param key the decryption key + * @param skip the number of bytes to skip at the beginning of the + * decryption + * @return an array of decrypted bytes. The length of the array + * will be n-skip. + */ + public byte[] getNEncodedBytes(int n, int key, int skip) { + byte[] result = decrypt(this.data, this.loc, this.loc + n, key, skip); + this.loc += n; + return result; + } + } + + /** + * get the index into the byte array of a slashed name, like "/name". + * @param d the search array + * @param name the name to look for, without the initial / + * @return the index of the first occurance of /name in the array. + */ + private int findSlashName(byte[] d, String name) { + int i; + for (i = 0; i < d.length; i++) { + if (d[i] == '/') { + // check for key + boolean found = true; + for (int j = 0; j < name.length(); j++) { + if (d[i + j + 1] != name.charAt(j)) { + found = false; + break; + } + } + if (found) { + return i; + } + } + } + return -1; + } + + /** + * get the character definitions of the font. + * @param d the font data + * @return a HashMap that maps string glyph names to byte arrays of + * decoded font data. + */ + private HashMap readChars(byte[] d) { + // skip thru data until we find "/"+key + HashMap hm = new HashMap(); + int i = findSlashName(d, "CharStrings"); + if (i < 0) { + // not found + return hm; + } + PSParser psp = new PSParser(d, i); + // read /name len -| [len bytes] |- + // until "end" + while (true) { + String s = psp.readThing(); + char c = s.charAt(0); + if (c == '/') { + int len = Integer.parseInt(psp.readThing()); + String go = psp.readThing(); // it's -| or RD + if (go.equals("-|") || go.equals("RD")) { + psp.setLoc(psp.getLoc() + 1); + byte[] line = psp.getNEncodedBytes(len, this.password, this.lenIV); + hm.put(s.substring(1), line); + } + } else if (s.equals("end")) { + break; + } + } + return hm; + } + + /** + * pop the next item off the stack + */ + private float pop() { + float val = 0; + if (this.sloc > 0) { + val = this.stack[--this.sloc]; + } + return val; + } + int callcount = 0; + + /** + * parse glyph data into a GeneralPath, and return the advance width. + * The working point is passed in as a parameter in order to allow + * recursion. + * @param cs the decrypted glyph data + * @param gp a GeneralPath into which the glyph shape will be stored + * @param pt a FlPoint object that will be used to generate the path + * @param wid a FlPoint into which the advance width will be placed. + */ + private void parse(byte[] cs, GeneralPath gp, FlPoint pt, FlPoint wid) { + int loc = 0; + float x1, x2, x3, y1, y2, y3; + boolean flexMode = false; + float[] flexArray = new float[16]; + int flexPt = 0; + while (loc < cs.length) { + int v = (cs[loc++]) & 0xff; + if (v == 255) { + this.stack[this.sloc++] = (((cs[loc]) & 0xff) << 24) + + (((cs[loc + 1]) & 0xff) << 16) + + (((cs[loc + 2]) & 0xff) << 8) + + (((cs[loc + 3]) & 0xff)); + loc += 4; + } else if (v >= 251) { + this.stack[this.sloc++] = -((v - 251) << 8) - ((cs[loc]) & 0xff) - 108; + loc++; + } else if (v >= 247) { + this.stack[this.sloc++] = ((v - 247) << 8) + ((cs[loc]) & 0xff) + 108; + loc++; + } else if (v >= 32) { + this.stack[this.sloc++] = v - 139; + } else { + switch (v) { + case 0: // x + throw new RuntimeException("Bad command (" + v + ")"); + case 1: // hstem + this.sloc = 0; + break; + case 2: // x + throw new RuntimeException("Bad command (" + v + ")"); + case 3: // vstem + this.sloc = 0; + break; + case 4: // y vmoveto + pt.y += pop(); + if (flexMode) { + flexArray[flexPt++] = pt.x; + flexArray[flexPt++] = pt.y; + } + else{ + gp.moveTo(pt.x, pt.y); + } + this.sloc = 0; + break; + case 5: // x y rlineto + pt.y += pop(); + pt.x += pop(); + gp.lineTo(pt.x, pt.y); + this.sloc = 0; + break; + case 6: // x hlineto + pt.x += pop(); + gp.lineTo(pt.x, pt.y); + this.sloc = 0; + break; + case 7: // y vlineto + pt.y += pop(); + gp.lineTo(pt.x, pt.y); + this.sloc = 0; + break; + case 8: // x1 y1 x2 y2 x3 y3 rcurveto + y3 = pop(); + x3 = pop(); + y2 = pop(); + x2 = pop(); + y1 = pop(); + x1 = pop(); + gp.curveTo(pt.x + x1, pt.y + y1, + pt.x + x1 + x2, pt.y + y1 + y2, + pt.x + x1 + x2 + x3, pt.y + y1 + y2 + y3); + pt.x += x1 + x2 + x3; + pt.y += y1 + y2 + y3; + this.sloc = 0; + break; + case 9: // closepath + gp.closePath(); + this.sloc = 0; + break; + case 10: // n callsubr + int n = (int) pop(); + if (n == 1) { + flexMode = true; + flexPt = 0; + this.sloc = 0; + break; + } + if (n == 0) { + if (flexPt != 14) { + PDFDebugger.debug("There must be 14 flex entries!"); + } + else { + gp.curveTo(flexArray[2], flexArray[3], flexArray[4], + flexArray[5], + flexArray[6], flexArray[7]); + gp.curveTo(flexArray[8], flexArray[9], flexArray[10], + flexArray[11], + flexArray[12], flexArray[13]); + flexMode = false; + this.sloc = 0; + //System.out.println("End Flex " + flexPt); + break; + } + } + if (n == 2) { + if (flexMode == false) { + PDFDebugger.debug("Flex mode assumed"); + } + else { + this.sloc = 0; + break; + } + } + if (this.subrs[n] == null) { + PDFDebugger.debug("No subroutine #" + n); + } else { + this.callcount++; + if (this.callcount > 10) { + PDFDebugger.debug("Call stack too large"); + } else { + parse(this.subrs[n], gp, pt, wid); + } + this.callcount--; + } + break; + case 11: // return + return; + case 12: // ext... + v = (cs[loc++]) & 0xff; + if (v == 6) { // s x y a b seac + char a = (char) pop(); + char b = (char) pop(); + float y = pop(); + float x = pop(); + buildAccentChar(x, y, a, b, gp); + this.sloc = 0; + } else if (v == 7) { // x y w h sbw + wid.y = pop(); + wid.x = pop(); + pt.y = pop(); + pt.x = pop(); + this.sloc = 0; + } else if (v == 12) { // a b div -> a/b + float b = pop(); + float a = pop(); + this.stack[this.sloc++] = a / b; + } else if (v == 33) { // a b setcurrentpoint + pt.y = pop(); + pt.x = pop(); + gp.moveTo(pt.x, pt.y); + this.sloc = 0; + } else if (v == 0) { // dotsection + this.sloc = 0; + } else if (v == 1) { // vstem3 + this.sloc = 0; + } else if (v == 2) { // hstem3 + this.sloc = 0; + } else if (v == 16) { // n callothersubr + int cn = (int) pop(); + int countargs = (int) pop(); + + switch (cn) { + case 0: + // push args2 and args3 onto stack + this.psStack[this.psLoc++] = pop(); + this.psStack[this.psLoc++] = pop(); + pop(); + break; + case 3: + // push 3 onto the postscript stack + this.psStack[this.psLoc++] = 3; + break; + default: + // push arguments onto the postscript stack + for (int i = 0; i > countargs; i--) { + this.psStack[this.psLoc++] = pop(); + } + break; + } + } else if (v == 17) { // pop + // pop from the postscript stack onto the type1 stack + this.stack[this.sloc++] = this.psStack[this.psLoc - 1]; + this.psLoc--; + } else { + throw new RuntimeException("Bad command (" + v + ")"); + } + break; + case 13: // s w hsbw + wid.x = pop(); + wid.y = 0; + pt.x = pop(); + pt.y = 0; + // gp.moveTo(pt.x, pt.y); + this.sloc = 0; + break; + case 14: // endchar + // return; + break; + case 15: // x + case 16: // x + case 17: // x + case 18: // x + case 19: // x + case 20: // x + throw new RuntimeException("Bad command (" + v + ")"); + case 21: // x y rmoveto + pt.y += pop(); + pt.x += pop(); + if (flexMode) { + flexArray[flexPt++] = pt.x; + flexArray[flexPt++] = pt.y; + } + else{ + gp.moveTo(pt.x, pt.y); + } + this.sloc = 0; + break; + case 22: // x hmoveto + pt.x += pop(); + if (flexMode) { + flexArray[flexPt++] = pt.x; + flexArray[flexPt++] = pt.y; + } + else { + gp.moveTo(pt.x, pt.y); + } + this.sloc = 0; + break; + case 23: // x + case 24: // x + case 25: // x + case 26: // x + case 27: // x + case 28: // x + case 29: // x + throw new RuntimeException("Bad command (" + v + ")"); + case 30: // y1 x2 y2 x3 vhcurveto + x3 = pop(); + y2 = pop(); + x2 = pop(); + y1 = pop(); + x1 = y3 = 0; + gp.curveTo(pt.x, pt.y + y1, + pt.x + x2, pt.y + y1 + y2, + pt.x + x2 + x3, pt.y + y1 + y2); + pt.x += x2 + x3; + pt.y += y1 + y2; + this.sloc = 0; + break; + case 31: // x1 x2 y2 y3 hvcurveto + y3 = pop(); + y2 = pop(); + x2 = pop(); + x1 = pop(); + y1 = x3 = 0; + gp.curveTo(pt.x + x1, pt.y, + pt.x + x1 + x2, pt.y + y2, + pt.x + x1 + x2, pt.y + y2 + y3); + pt.x += x1 + x2; + pt.y += y2 + y3; + this.sloc = 0; + break; + } + } + } + } + + /** + * build an accented character out of two pre-defined glyphs. + * @param x the x offset of the accent relativ to the sidebearing of the base char + * @param y the y offset of the accent relativ to the sidebearing of the base char + * @param a the index of the accent glyph + * @param b the index of the base glyph + * @param gp the GeneralPath into which the combined glyph will be + * written. + */ + private void buildAccentChar(float x, float y, char a, char b, + GeneralPath gp) { + // get the outline of the accent + GeneralPath pathA = getOutline(a, getWidth(a, null)); + // don't manipulate the original glyph + pathA = (GeneralPath) pathA.clone(); + try { + final AffineTransform xformA = at.createInverse(); + pathA.transform(xformA); + // Best x can't be calculated cause we don't know the left sidebearing of the base character. + // Leaving x=0 gives the best results. + // see Chapter 6 of http://partners.adobe.com/public/developer/en/font/5015.Type1_Supp.pdf + // and the definition of the seac-Command in http://partners.adobe.com/public/developer/en/font/T1_SPEC.PDF + final AffineTransform xformA2 = AffineTransform.getTranslateInstance(0, y); + pathA.transform(xformA2); + } catch (NoninvertibleTransformException nte) { + pathA.transform(AffineTransform.getTranslateInstance(x, y)); + } + + GeneralPath pathB = getOutline(b, getWidth(b, null)); + + try { + AffineTransform xformB = this.at.createInverse(); + pathB.transform(xformB); + } catch (NoninvertibleTransformException nte) { + // ignore + } + + gp.append(pathB, false); + gp.append(pathA, false); + } + + /** + * Get the width of a given character + * + * This method is overridden to work if the width array hasn't been + * populated (as for one of the 14 base fonts) + */ + @Override + public float getWidth(char code, String name) { + // we don't have first and last chars, so therefore no width array + if (getFirstChar() == -1 || getLastChar() == -1) { + String key = this.chr2name[code & 0xff]; + + // use a name if one is provided + if (name != null) { + key = name; + } + + if (key != null && this.name2outline.containsKey(key)) { + if (!this.name2width.containsKey(key)) { + // glyph has not yet been parsed + // getting the outline will force it to get read + getOutline(key, 0); + } + + FlPoint width = this.name2width.get(key); + if (width != null) { + return width.x / getDefaultWidth(); + } + } + + return 0; + } + + // return the width that has been specified + return super.getWidth(code, name); + } + + /** + * Decrypt a glyph stored in byte form + */ + private synchronized GeneralPath parseGlyph(byte[] cs, FlPoint advance, + AffineTransform at) { + GeneralPath gp = new GeneralPath(); + FlPoint curpoint = new FlPoint(); + + this.sloc = 0; + parse(cs, gp, curpoint, advance); + + gp.transform(at); + return gp; + } + + /** + * Get a glyph outline by name + * + * @param name the name of the desired glyph + * @return the glyph outline, or null if unavailable + */ + @Override + protected GeneralPath getOutline(String name, float width) { + // make sure we have a valid name + if (name == null || !this.name2outline.containsKey(name)) { + name = ".notdef"; + } + + // get whatever is stored in name. Could be a GeneralPath, could be byte[] + Object obj = this.name2outline.get(name); + + // if it's a byte array, it needs to be parsed + // otherwise, just return the path + if (obj instanceof GeneralPath) { + return (GeneralPath) obj; + } else { + byte[] cs = (byte[]) obj; + FlPoint advance = new FlPoint(); + + GeneralPath gp = parseGlyph(cs, advance, this.at); + + if (width != 0 && advance.x != 0) { + // scale the glyph to fit in the width + Point2D p = new Point2D.Float(advance.x, advance.y); + this.at.transform(p, p); + + double scale = width / p.getX(); + AffineTransform xform = AffineTransform.getScaleInstance(scale, 1.0); + gp.transform(xform); + } + + // put the parsed object in the cache + this.name2outline.put(name, gp); + this.name2width.put(name, advance); + return gp; + } + } + + /** + * Get a glyph outline by character code + * + * Note this method must always return an outline + * + * @param src the character code of the desired glyph + * @return the glyph outline + */ + @Override + protected GeneralPath getOutline(char src, float width) { + return getOutline(this.chr2name[src & 0xff], width); + } + + public boolean isName2OutlineFilled() { + return (name2outline!=null) && !name2outline.isEmpty(); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/Type3Font.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/Type3Font.java new file mode 100644 index 0000000000..d775657976 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/Type3Font.java @@ -0,0 +1,176 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.font; + +import java.awt.geom.AffineTransform; +import java.awt.geom.GeneralPath; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import com.github.librepdf.pdfrenderer.BaseWatchable; +import com.github.librepdf.pdfrenderer.PDFDebugger; +import com.github.librepdf.pdfrenderer.PDFFile; +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFPage; +import com.github.librepdf.pdfrenderer.PDFParser; + +/** + * A Type 3 Font, in which each glyph consists of a sequence of PDF + * commands. + * + * @author Mike Wessler + */ +public class Type3Font extends PDFFont { + + /** resources for the character definitions */ + HashMap rsrc; + /** the character processes, mapped by name */ + Map charProcs; + /** bounding box for the font characters */ + Rectangle2D bbox; + /** affine transform for the font characters */ + AffineTransform at; + /** the widths */ + float[] widths; + /** the start code */ + int firstChar; + /** the end code */ + int lastChar; + + /** + * Generate a Type 3 font. + * @param baseFont the postscript name of this font + * @param fontObj a dictionary containing references to the character + * definitions and font information + * @param resources a set of resources used by the character definitions + * @param descriptor the descriptor for this font + */ + public Type3Font(String baseFont, PDFObject fontObj, + HashMap resources, PDFFontDescriptor descriptor) throws IOException { + super(baseFont, descriptor); + + this.rsrc = new HashMap(); + + if (resources != null) { + this.rsrc.putAll(resources); + } + + // get the transform matrix + PDFObject matrix = fontObj.getDictRef("FontMatrix"); + float matrixAry[] = new float[6]; + for (int i = 0; i < 6; i++) { + matrixAry[i] = matrix.getAt(i).getFloatValue(); + } + this.at = new AffineTransform(matrixAry); + + // get the scale from the matrix + float scale = matrixAry[0] + matrixAry[2]; + + // put all the resources in a Hash + PDFObject rsrcObj = fontObj.getDictRef("Resources"); + if (rsrcObj != null) { + this.rsrc.putAll(rsrcObj.getDictionary()); + } + + // get the character processes, indexed by name + this.charProcs = fontObj.getDictRef("CharProcs").getDictionary(); + + // get the font bounding box + bbox = PDFFile.parseNormalisedRectangle(fontObj.getDictRef("FontBBox")); + if (bbox.isEmpty()) { + bbox = null; + } + + // get the widths + PDFObject[] widthArray = fontObj.getDictRef("Widths").getArray(); + this.widths = new float[widthArray.length]; + for (int i = 0; i < widthArray.length; i++) { + this.widths[i] = widthArray[i].getFloatValue(); + } + + // get first and last chars + this.firstChar = fontObj.getDictRef("FirstChar").getIntValue(); + this.lastChar = fontObj.getDictRef("LastChar").getIntValue(); + } + + /** + * Get the first character code + */ + public int getFirstChar() { + return this.firstChar; + } + + /** + * Get the last character code + */ + public int getLastChar() { + return this.lastChar; + } + + /** + * Get the glyph for a given character code and name + * + * The preferred method of getting the glyph should be by name. If the + * name is null or not valid, then the character code should be used. + * If the both the code and the name are invalid, the undefined glyph + * should be returned. + * + * Note this method must *always* return a glyph. + * + * @param src the character code of this glyph + * @param name the name of this glyph or null if unknown + * @return a glyph for this character + */ + @Override + protected PDFGlyph getGlyph(char src, String name) { + if (name == null) { + throw new IllegalArgumentException("Glyph name required for Type3 font!" + + "Source character: " + (int) src); + } + + PDFObject pageObj = (PDFObject) this.charProcs.get(name); + if (pageObj == null) { + // glyph not found. Return an empty glyph... + return new PDFGlyph(src, name, new GeneralPath(), new Point2D.Float(0, 0)); + } + + try { + PDFPage page = new PDFPage(this.bbox, 0); + page.addXform(this.at); + + PDFParser prc = new PDFParser(page, pageObj.getStream(), this.rsrc); + prc.go(true); + + float width = this.widths[src - this.firstChar]; + + Point2D advance = new Point2D.Float(width, 0); + advance = this.at.transform(advance, null); + + return new PDFGlyph(src, name, page, advance); + } catch (IOException ioe) { + // help! + PDFDebugger.debug("IOException in Type3 font: " + ioe); + BaseWatchable.getErrorHandler().publishException(ioe); + return null; + } + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/cid/PDFCMap.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/cid/PDFCMap.java new file mode 100644 index 0000000000..9d5a0bd22e --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/cid/PDFCMap.java @@ -0,0 +1,108 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.font.cid; + +import java.io.IOException; +import java.util.HashMap; + +import com.github.librepdf.pdfrenderer.PDFDebugger; +import com.github.librepdf.pdfrenderer.PDFObject; + +/** + * A CMap maps from a character in a composite font to a font/glyph number + * pair in a CID font. + * + * @author jkaplan + */ +public abstract class PDFCMap { + /** + * A cache of known CMaps by name + */ + private static HashMap cache; + + /** Creates a new instance of CMap */ + protected PDFCMap() {} + + /** + * Get a CMap, given a PDF object containing one of the following: + * a string name of a known CMap + * a stream containing a CMap definition + */ + public static PDFCMap getCMap(PDFObject map) throws IOException { + if (map.getType() == PDFObject.NAME) { + return getCMap(map.getStringValue()); + } else if (map.getType() == PDFObject.STREAM) { + return parseCMap(map); + } else { + throw new IOException("CMap type not Name or Stream!"); + } + } + + /** + * Get a CMap, given a string name + */ + public static PDFCMap getCMap(String mapName) throws IOException { + if (cache == null) { + populateCache(); + } + + if (!cache.containsKey(mapName)) { + //throw new IOException("Unknown CMap: " + mapName); + PDFDebugger.debug("Unknown CMap: '" + mapName + "' procced with 'Identity-H'"); + return cache.get("Identity-H"); + } + + return cache.get(mapName); + } + + /** + * Populate the cache with well-known types + */ + protected static void populateCache() { + cache = new HashMap(); + + // add the Identity-H map + cache.put("Identity-H", new PDFCMap() { + @Override + public char map(char src) { + return src; + } + }); + } + + /** + * Parse a CMap from a CMap stream + */ + protected static PDFCMap parseCMap(PDFObject map) throws IOException { + return new ToUnicodeMap(map); + } + + /** + * Map a given source character to a destination character + */ + public abstract char map(char src); + + /** + * Get the font number assoicated with a given source character + */ + public int getFontID(char src) { + return 0; + } + +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/cid/ToUnicodeMap.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/cid/ToUnicodeMap.java new file mode 100644 index 0000000000..f26ad94c92 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/cid/ToUnicodeMap.java @@ -0,0 +1,310 @@ +package com.github.librepdf.pdfrenderer.font.cid; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.github.librepdf.pdfrenderer.PDFObject; + +/***************************************************************************** + * Parses a CMAP and builds a lookup table to map CMAP based codes to unicode. + * This is not a fully functional CMAP parser but a stripped down parser + * that should be able to parse some limited variants of CMAPs that are + * used for the ToUnicode mapping found for some Type0 fonts. + * + * @author Bernd Rosstauscher + * @since 03.08.2011 + ****************************************************************************/ + +public class ToUnicodeMap extends PDFCMap { + + /***************************************************************************** + * Small helper class to define a code range. + ****************************************************************************/ + + private static class CodeRangeMapping { + char srcStart; + char srcEnd; + + CodeRangeMapping(char srcStart, char srcEnd) { + this.srcStart = srcStart; + this.srcEnd = srcEnd; + } + + boolean contains(char c) { + return this.srcStart <= c + && c <= this.srcEnd; + } + + } + + /***************************************************************************** + * Small helper class to define a char range. + ****************************************************************************/ + + private static class CharRangeMapping { + char srcStart; + char srcEnd; + char destStart; + + CharRangeMapping(char srcStart, char srcEnd, char destStart) { + this.srcStart = srcStart; + this.srcEnd = srcEnd; + this.destStart = destStart; + } + + boolean contains(char c) { + return this.srcStart <= c + && c <= this.srcEnd; + } + + char map(char src) { + return (char) (this.destStart + (src-this.srcStart)); + } + + } + + private final Map singleCharMappings; + private final List charRangeMappings; + private final List codeRangeMappings; + + /************************************************************************* + * Constructor + * @param map + * @throws IOException + ************************************************************************/ + + public ToUnicodeMap(PDFObject map) throws IOException { + super(); + this.singleCharMappings = new HashMap(); + this.charRangeMappings = new ArrayList(); + this.codeRangeMappings = new ArrayList(); + parseMappings(map); + } + + /************************************************************************* + * @param map + * @throws IOException + ************************************************************************/ + + private void parseMappings(PDFObject map) throws IOException { + try { + StringReader reader = new StringReader(new String(map.getStream(), "ASCII")); + BufferedReader bf = new BufferedReader(reader); + String line = bf.readLine(); + while (line != null) { + if (line.contains("beginbfchar") || line.contains("begincidchar")) { + parseSingleCharMappingSection(bf, line.contains("begincidchar")); + } + if (line.contains("beginbfrange") || line.contains("begincidrange")) { + parseCharRangeMappingSection(bf, line.contains("begincidrange")); + } + if (line.contains("begincodespacerange")) { + parseCodeRangeMappingSection(bf, line); + } + line = bf.readLine(); + } + } catch (UnsupportedEncodingException e) { + throw new IOException(e); + } + } + + /************************************************************************* + * @param bf + * @throws IOException + ************************************************************************/ + + private void parseCharRangeMappingSection(BufferedReader bf, boolean isCid) throws IOException { + String line = bf.readLine(); + while (line != null) { + if (line.contains("endbfrange") || line.contains("endcidrange")) { + break; + } + parseRangeLine(line, isCid); + line = bf.readLine(); + } + } + + private void parseCodeRangeMappingSection(BufferedReader bf, String line) throws IOException { + //check if the prev line contains "endcodespacerange" + if(line.contains("endcodespacerange")) { + int indexOf = line.indexOf("endcodespacerange"); + line = line.substring(0, indexOf); + indexOf = line.indexOf("begincodespacerange"); + line = line.substring(indexOf+"begincodespacerange".length(), line.length()); + line = line.trim(); + + parseCodeRangeLine(line); + } + else { + String rline = bf.readLine(); + while (rline != null) { + if (rline.contains("endcodespacerange")) { + break; + } + parseCodeRangeLine(rline); + rline = bf.readLine(); + } + } + } + + /************************************************************************* + * @param line + * @return + ************************************************************************/ + + private void parseRangeLine(String line, boolean isCid) { + String[] mapping = line.split(" "); + if (mapping.length == 3) { + Character srcStart = parseChar(mapping[0]); + Character srcEnd = parseChar(mapping[1]); + Character destStart; + if(isCid) { + destStart = (char)Integer.parseInt(mapping[2]); + } + else { + destStart = parseChar(mapping[2]); + } + this.charRangeMappings.add(new CharRangeMapping(srcStart, srcEnd, destStart)); + } + else { + int indexOf1 = line.indexOf(">"); + String substring1 = line.substring(0, indexOf1+1); + + int indexOf2 = line.indexOf("<", indexOf1); + int indexOf3 = line.indexOf(">", indexOf2); + String substring2 = line.substring(indexOf2, indexOf3+1); + + int indexOf4 = line.indexOf("<", indexOf3); + String substring3 = line.substring(indexOf4, line.length()); + + if(!substring1.isEmpty() && !substring2.isEmpty() && !substring3.isEmpty()) { + Character srcStart = parseChar(substring1); + Character srcEnd = parseChar(substring2); + Character destStart = parseChar(substring3); + this.charRangeMappings.add(new CharRangeMapping(srcStart, srcEnd, destStart)); + } + } + } + + private void parseCodeRangeLine(String line) { + String[] mapping = line.split(" "); + if (mapping.length == 2) { + Character srcStart = parseChar(mapping[0]); + Character srcEnd = parseChar(mapping[1]); + this.codeRangeMappings.add(new CodeRangeMapping(srcStart, srcEnd)); + } + else { + int indexOf1 = line.indexOf(">"); + String substring1 = line.substring(0, indexOf1+1); + + int indexOf2 = line.indexOf("<", indexOf1); + String substring2 = line.substring(indexOf2, line.length()); + + if(!substring1.isEmpty() && !substring2.isEmpty()) { + Character srcStart = parseChar(substring1); + Character srcEnd = parseChar(substring2); + this.codeRangeMappings.add(new CodeRangeMapping(srcStart, srcEnd)); + } + } + } + + /************************************************************************* + * @param bf + * @throws IOException + ************************************************************************/ + + private void parseSingleCharMappingSection(BufferedReader bf, boolean isCID) throws IOException { + String line = bf.readLine(); + while (line != null) { + if (line.contains("endbfchar") || line.contains("endcidchar")) { + break; + } + parseSingleCharMappingLine(line, isCID); + line = bf.readLine(); + } + } + + /************************************************************************* + * @param line + * @return + ************************************************************************/ + + private void parseSingleCharMappingLine(String line, boolean isCID) { + String[] mapping = line.split(" "); + if (mapping.length == 2) { + if(isCID) { + this.singleCharMappings.put(parseChar(mapping[0]), (char)Integer.parseInt(mapping[1])); + } + else { + this.singleCharMappings.put(parseChar(mapping[0]), parseChar(mapping[1])); + } + } + } + + /************************************************************************* + * Parse a string of the format <0F3A> to a char. + * @param charDef + * @return + ************************************************************************/ + + private Character parseChar(String charDef) { + if (charDef.startsWith("<")) { + charDef = charDef.substring(1); + } + if (charDef.endsWith(">")) { + charDef = charDef.substring(0, charDef.length()-1); + } + try { + long result = Long.decode("0x" + charDef); + return (char) result; + } catch (NumberFormatException e) { + return (char) ' '; + } + } + + /************************************************************************* + * map + * @see PDFCMap#map(char) + ************************************************************************/ + @Override + public char map(char src) { + Character mappedChar = null; + for (CodeRangeMapping codeRange : this.codeRangeMappings) { + if(codeRange.contains(src)) { + mappedChar = this.singleCharMappings.get(src); + if (mappedChar == null) { + mappedChar = lookupInRanges(src); + } + break; + } + } + if (mappedChar == null) { + // TODO XOND 27.03.2012: PDF Spec. "9.7.6.3Handling Undefined Characters" + mappedChar = 0; + } + return mappedChar; + } + + /************************************************************************* + * @param src + * @return + ************************************************************************/ + + private Character lookupInRanges(char src) { + Character mappedChar = null; + for (CharRangeMapping rangeMapping : this.charRangeMappings) { + if (rangeMapping.contains(src)) { + mappedChar = rangeMapping.map(src); + break; + } + } + return mappedChar; + } + +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/AdobeGlyphList.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/AdobeGlyphList.java new file mode 100644 index 0000000000..db2f7121ba --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/AdobeGlyphList.java @@ -0,0 +1,181 @@ +package com.github.librepdf.pdfrenderer.font.ttf; + +import java.io.*; +import java.util.*; + +/** + * Build an object which provides access to all the Adobe glyph names, using + * a unicode value, and which can translate a glyph name to one or more + * unicode values. + * +# ################################################################################### +# 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 + * + * @author tomoke + */ +public class AdobeGlyphList { + + /** provide a translation from a glyph name to the possible unicode values. */ + static private HashMap glyphToUnicodes; + /** provide a translation from a unicode value to a glyph name. */ + static private HashMap unicodeToGlyph; + /** the loader thread we are reading through. */ + static Thread glyphLoaderThread = null; + + + static { + new AdobeGlyphList(); + } + + /** + *

    private constructor to restrict creation to a singleton.

    + * + *

    We initialize by creating the storage and parsing the glyphlist + * into the tables.

    + */ + private AdobeGlyphList() { + glyphToUnicodes = new HashMap(4500); + unicodeToGlyph = new HashMap(4500); + glyphLoaderThread = new Thread(new Runnable() { + + @Override + public void run() { + int[] codes; + StringTokenizer codeTokens; + String glyphName; + StringTokenizer tokens; + ArrayList unicodes = new ArrayList(); + + InputStream istr = getClass().getResourceAsStream("/com/sun/pdfview/font/ttf/resource/glyphlist.txt"); + + BufferedReader reader = new BufferedReader(new InputStreamReader(istr)); + String line = ""; + while (line != null) { + try { + unicodes.clear(); + line = reader.readLine(); + if (line == null) { + break; + } + line = line.trim(); + if (line.length() > 0 && !line.startsWith("#")) { + // ignore comment lines + tokens = new StringTokenizer(line, ";"); + glyphName = tokens.nextToken(); + codeTokens = new StringTokenizer(tokens.nextToken(), " "); + while (codeTokens.hasMoreTokens()) { + unicodes.add(codeTokens.nextToken()); + } + codes = new int[unicodes.size()]; + for (int i = 0; i < unicodes.size(); i++) { + codes[i] = Integer.parseInt(unicodes.get(i), 16); + unicodeToGlyph.put(Integer.valueOf(codes[i]), glyphName); + } + glyphToUnicodes.put(glyphName, codes); + } + + } catch (IOException ex) { + break; + } + } + } + }, "Adobe Glyph Loader Thread"); + glyphLoaderThread.setDaemon(true); + glyphLoaderThread.setPriority(Thread.MIN_PRIORITY); + glyphLoaderThread.start(); + } + + /** + * translate a glyph name into the possible unicode values that it + * might represent. It is possible to have more than one unicode + * value for a single glyph name. + * + * @param glyphName + * @return int[] + */ + public static int[] getUnicodeValues(String glyphName) { + while (glyphLoaderThread != null && glyphLoaderThread.isAlive()) { + synchronized (glyphToUnicodes) { + try { + glyphToUnicodes.wait(250); + } catch (InterruptedException ex) { + // ignore + } + } + } + return glyphToUnicodes.get(glyphName); + } + + /** + * return a single index for a glyph, though there may be multiples. + * + * @param glyphName + * @return Integer + */ + public static Integer getGlyphNameIndex(String glyphName) { + int [] unicodes = getUnicodeValues(glyphName); + if (unicodes == null) { + return null; + } else { + return Integer.valueOf(unicodes[0]); + } + } + + /** + * translate a unicode value into a glyph name. It is possible for + * different unicode values to translate into the same glyph name. + * + * @param unicode + * @return String + */ + public static String getGlyphName(int unicode) { + while (glyphLoaderThread != null && glyphLoaderThread.isAlive()) { + synchronized (glyphToUnicodes) { + try { + glyphToUnicodes.wait(250); + } catch (InterruptedException ex) { + // ignore + } + } + } + return unicodeToGlyph.get(Integer.valueOf(unicode)); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/CMap.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/CMap.java new file mode 100644 index 0000000000..8e13fbd07f --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/CMap.java @@ -0,0 +1,185 @@ +/* + * $Id: CMap.java,v 1.1 2009-07-01 12:43:20 bros Exp $ + * + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.font.ttf; + +import java.nio.ByteBuffer; + +import com.github.librepdf.pdfrenderer.PDFDebugger; + +/** + * + * @author jkaplan + */ +public abstract class CMap { + + /** + * The format of this map + */ + private final short format; + + /** + * The language of this map, or 0 for language-independent + */ + private final short language; + + /** Creates a new instance of CMap + * Don't use this directly, use CMap.createMap() + */ + protected CMap (short format, short language) { + this.format = format; + this.language = language; + } + + /** + * Create a map for the given format and language + + *

    The Macintosh standard character to glyph mapping is supported + * by format 0.

    + * + *

    Format 2 supports a mixed 8/16 bit mapping useful for Japanese, + * Chinese and Korean.

    + * + *

    Format 4 is used for 16 bit mappings.

    + * + *

    Format 6 is used for dense 16 bit mappings.

    + * + *

    Formats 8, 10, and 12 (properly 8.0, 10.0, and 12.0) are used + * for mixed 16/32-bit and pure 32-bit mappings.
    + * This supports text encoded with surrogates in Unicode 2.0 and later.

    + * + *

    Reference:
    + * http://developer.apple.com/textfonts/TTRefMan/RM06/Chap6cmap.html

    + */ + public static CMap createMap (short format, short language) { + CMap outMap = null; + + switch (format) { + case 0: // CMap format 0 - single byte codes + outMap = new CMapFormat0 (language); + break; + case 4: // CMap format 4 - two byte encoding + outMap = new CMapFormat4 (language); + break; + case 6: // CMap format 6 - 16-bit, two byte encoding + outMap = new CMapFormat6 (language); + break; +// case 8: // CMap format 8 - Mixed 16-bit and 32-bit coverage +// outMap = new CMapFormat_8(language); +// break; +// // CMap format 10 - Format 10.0 is a bit like format 6, in that it +// // defines a trimmed array for a tight range of 32-bit character codes: +// case 10: +// outMap = new CMapFormat_10(language); +// break; +// // Format 12.0 is a bit like format 4, in that it defines +// // segments for sparse representation in 4-byte character space. +// case 12: // CMap format 12 - +// outMap = new CMapFormat_12(language); +// break; + default: + PDFDebugger.debug("Unsupport CMap format: " + format); + return null; + } + + return outMap; + } + + /** + * Get a map from the given data + * + * This method reads the format, data and length variables of + * the map. + */ + public static CMap getMap (ByteBuffer data) { + short format = data.getShort (); + short lengthShort = data.getShort (); + int length = 0xFFFF & lengthShort; + PDFDebugger.debug("CMAP, length: " + length + ", short: " + lengthShort, 100); + + // make sure our slice of the data only contains up to the length + // of this table + data.limit (Math.min (length, data.limit ())); + + short language = data.getShort (); + + CMap outMap = createMap (format, language); + if (outMap == null) { + return null; + } + + outMap.setData (data.limit (), data); + + return outMap; + } + + /** + * Get the format of this map + */ + public short getFormat () { + return this.format; + } + + /** + * Get the language of this map + */ + public short getLanguage () { + return this.language; + } + + /** + * Set the data for this map + */ + public abstract void setData (int length, ByteBuffer data); + + /** + * Get the data in this map as a byte buffer + */ + public abstract ByteBuffer getData (); + + /** + * Get the length of this map + */ + public abstract short getLength (); + + /** + * Map an 8 bit value to another 8 bit value + */ + public abstract byte map (byte src); + + /** + * Map a 16 bit value to another 16 but value + */ + public abstract char map (char src); + + /** + * Get the src code which maps to the given glyphID + */ + public abstract char reverseMap (short glyphID); + + /** Print a pretty string */ + @Override + public String toString () { + String indent = " "; + + return indent + " format: " + getFormat () + " length: " + + getLength () + " language: " + getLanguage () + "\n"; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/CMapFormat0.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/CMapFormat0.java new file mode 100644 index 0000000000..e935cf1228 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/CMapFormat0.java @@ -0,0 +1,157 @@ +/* + * $Id: CMapFormat0.java,v 1.1 2009-07-01 12:43:20 bros Exp $ + * + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.font.ttf; + +import java.nio.ByteBuffer; + +/** + * + * @author jkaplan + */ +public class CMapFormat0 extends CMap { + + /** + * The glyph index array + */ + private byte[] glyphIndex; + + /** Creates a new instance of CMapFormat0 */ + protected CMapFormat0(short language) { + super((short) 0, language); + + byte[] initialIndex = new byte[256]; + for (int i = 0; i < initialIndex.length; i++) { + initialIndex[i] = (byte) i; + } + setMap(initialIndex); + } + + /** + * Get the length of this table + */ + @Override + public short getLength() { + return (short) 262; + } + + /** + * Map from a byte + */ + @Override + public byte map(byte src) { + int i = 0xff & src; + + return this.glyphIndex[i]; + } + + /** + * Cannot map from short + */ + @Override + public char map(char src) { + if (src < 0 || src > 255) { + // out of range + return (char) 0; + } + + return (char) (map((byte) src) & 0xff); + } + + + /** + * Get the src code which maps to the given glyphID + */ + @Override + public char reverseMap(short glyphID) { + for (int i = 0; i < this.glyphIndex.length; i++) { + if ((this.glyphIndex[i] & 0xff) == glyphID) { + return (char) i; + } + } + + return (char) 0; + } + + /** + * Set the entire map + */ + public void setMap(byte[] glyphIndex) { + if (glyphIndex.length != 256) { + throw new IllegalArgumentException("Glyph map must be size 256!"); + } + + this.glyphIndex = glyphIndex; + } + + /** + * Set a single mapping entry + */ + public void setMap(byte src, byte dest) { + int i = 0xff & src; + + this.glyphIndex[i] = dest; + } + + /** + * Get the whole map + */ + protected byte[] getMap() { + return this.glyphIndex; + } + + /** + * Get the data in this map as a ByteBuffer + */ + @Override + public ByteBuffer getData() { + ByteBuffer buf = ByteBuffer.allocate(262); + + buf.putShort(getFormat()); + buf.putShort(getLength()); + buf.putShort(getLanguage()); + buf.put(getMap()); + + // reset the position to the beginning of the buffer + buf.flip(); + + return buf; + } + + /** + * Read the map in from a byte buffer + */ + @Override + public void setData(int length, ByteBuffer data) { + if (length != 262) { + throw new IllegalArgumentException("Bad length for CMap format 0"); + } + + if (data.remaining() != 256) { + throw new IllegalArgumentException("Wrong amount of data for CMap format 0"); + } + + byte[] map = new byte[256]; + data.get(map); + + setMap(map); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/CMapFormat4.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/CMapFormat4.java new file mode 100644 index 0000000000..92fec6cdc7 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/CMapFormat4.java @@ -0,0 +1,464 @@ +/* + * $Id: CMapFormat4.java,v 1.3 2011-04-15 15:44:14 xphc Exp $ + * + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.font.ttf; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.Iterator; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * + * @author jkaplan + */ +public class CMapFormat4 extends CMap { + + /** + * The segments and associated data can be a char[] or an Integer + */ + public SortedMap segments; + + /** Creates a new instance of CMapFormat0 */ + protected CMapFormat4(short language) { + super((short) 4, language); + + this.segments = Collections.synchronizedSortedMap(new TreeMap()); + + char[] map = new char[1]; + map[0] = (char) 0; + addSegment((short) 0xffff, (short) 0xffff, map); + } + + /** + * Add a segment with a map + */ + public void addSegment(short startCode, short endCode, char[] map) { + if (map.length != (endCode - startCode) + 1) { + throw new IllegalArgumentException("Wrong number of entries in map"); + } + + Segment s = new Segment(startCode, endCode, true); + // make sure we remove any old entries + this.segments.remove(s); + this.segments.put(s, map); + } + + /** + * Add a segment with an idDelta + */ + public void addSegment(short startCode, short endCode, short idDelta) { + Segment s = new Segment(startCode, endCode, false); + // make sure we remove any old entries + this.segments.remove(s); + this.segments.put(s, Integer.valueOf(idDelta)); + } + + /** + * Remove a segment + */ + public void removeSegment(short startCode, short endCode) { + Segment s = new Segment(startCode, endCode, true); + this.segments.remove(s); + } + + /** + * Get the length of this table + */ + @Override + public short getLength() { + // start with the size of the fixed header + short size = 16; + + // add the size of each segment header + size += this.segments.size() * 8; + + // add the total number of mappings times the size of a mapping + for (Iterator i = this.segments.keySet().iterator(); i.hasNext();) { + Segment s = (Segment) i.next(); + + // see if there's a map + if (s.hasMap) { + // if there is, add its size + char[] map = (char[]) this.segments.get(s); + size += map.length * 2; + } + } + + return size; + } + + /** + * Cannot map from a byte + */ + @Override + public byte map(byte src) { + char c = map((char) src); + if (c < Byte.MIN_VALUE || c > Byte.MAX_VALUE) { + // out of range + return 0; + } + + return (byte) c; + } + + /** + * Map from char + */ + @Override + public char map(char src) { + // find first segment with endcode > src + for (Iterator i = this.segments.keySet().iterator(); i.hasNext();) { + Segment s = (Segment) i.next(); + + if (s.endCode >= src) { + // are we within range? + if (s.startCode <= src) { + if (s.hasMap) { + // return the index of this character in + // the segment's map + char[] map = (char[]) this.segments.get(s); + return map[src - s.startCode]; + } else { + // return the character code + idDelta + Integer idDelta = (Integer) this.segments.get(s); + return (char) (src + idDelta.intValue()); + } + } else { + // undefined character + return (char) 0; + } + } + } + + // shouldn't get here! + return (char) 0; + } + + /** + * Get the src code which maps to the given glyphID + */ + @Override + public char reverseMap(short glyphID) { + // look at each segment + for (Iterator i = this.segments.keySet().iterator(); i.hasNext();) { + Segment s = (Segment) i.next(); + + // see if we have a map or a delta + if (s.hasMap) { + char[] map = (char[]) this.segments.get(s); + + // if we have a map, we have to iterate through it + for (int c = 0; c < map.length; c++) { + if (map[c] == glyphID) { + return (char) (s.startCode + c); + } + } + } else { + Integer idDelta = (Integer) this.segments.get(s); + + // we can do the math to see if we're in range + int start = s.startCode + idDelta.intValue(); + int end = s.endCode + idDelta.intValue(); + + if (glyphID >= start && glyphID <= end) { + // we're in the range + return (char) (glyphID - idDelta.intValue()); + } + } + } + + // not found! + return (char) 0; + } + + + /** + * Get the data in this map as a ByteBuffer + */ + @Override + public void setData(int length, ByteBuffer data) { + // read the table size values + short segCount = (short) (data.getShort() / 2); + short searchRange = data.getShort(); + short entrySelector = data.getShort(); + short rangeShift = data.getShort(); + + // create arrays to store segment info + short[] endCodes = new short[segCount]; + short[] startCodes = new short[segCount]; + short[] idDeltas = new short[segCount]; + short[] idRangeOffsets = new short[segCount]; + + // the start of the glyph array + int glyphArrayPos = 16 + (8 * segCount); + + // read the endCodes + for (int i = 0; i < segCount; i++) { + endCodes[i] = data.getShort(); + } + + // read the pad + data.getShort(); + + // read the start codes + for (int i = 0; i < segCount; i++) { + startCodes[i] = data.getShort(); + } + + // read the idDeltas + for (int i = 0; i < segCount; i++) { + idDeltas[i] = data.getShort(); + } + + // read the id range offsets + for (int i = 0; i < segCount; i++) { + idRangeOffsets[i] = data.getShort(); + + // calculate the actual offset + if (idRangeOffsets[i] <= 0) { + // the easy way + addSegment(startCodes[i], endCodes[i], idDeltas[i]); + } else { + // find the start of the data segment + int offset = (data.position() - 2) + idRangeOffsets[i]; + + // get the number of entries in the map + int size = (endCodes[i] - startCodes[i]) + 1; + + // allocate the actual map + char[] map = new char[size]; + + // remember our offset + data.mark(); + + // read the mappings + for (int c = 0; c < size; c++) { + data.position(offset + (c * 2)); + map[c] = data.getChar(); + } + + // reset the position + data.reset(); + + addSegment(startCodes[i], endCodes[i], map); + } + } + } + + /** + * Get the data in the map as a byte buffer + */ + @Override + public ByteBuffer getData() { + ByteBuffer buf = ByteBuffer.allocate(getLength()); + + // write the header + buf.putShort(getFormat()); + buf.putShort(getLength()); + buf.putShort(getLanguage()); + + // write the various values + buf.putShort((short) (getSegmentCount() * 2)); + buf.putShort(getSearchRange()); + buf.putShort(getEntrySelector()); + buf.putShort(getRangeShift()); + + // write the endCodes + for (Iterator i = this.segments.keySet().iterator(); i.hasNext();) { + Segment s = i.next(); + buf.putShort((short) s.endCode); + } + + // write the pad + buf.putShort((short) 0); + + // write the startCodes + for (Iterator i = this.segments.keySet().iterator(); i.hasNext();) { + Segment s = i.next(); + buf.putShort((short) s.startCode); + } + + // write the idDeltas for segments using deltas + for (Iterator i = this.segments.keySet().iterator(); i.hasNext();) { + Segment s = i.next(); + + if (!s.hasMap) { + Integer idDelta = (Integer) this.segments.get(s); + buf.putShort(idDelta.shortValue()); + } else { + buf.putShort((short) 0); + } + } + + // the start of the glyph array + int glyphArrayOffset = 16 + (8 * getSegmentCount()); + + // write the idRangeOffsets and maps for segments using maps + for (Iterator i = this.segments.keySet().iterator(); i.hasNext();) { + Segment s = i.next(); + + if (s.hasMap) { + // first set the offset, which is the number of bytes from the + // current position to the current offset + buf.putShort((short) (glyphArrayOffset - buf.position())); + + // remember the current position + buf.mark(); + + // move the position to the offset + buf.position(glyphArrayOffset); + + // now write the map + char[] map = (char[]) this.segments.get(s); + for (int c = 0; c < map.length; c++) { + buf.putChar(map[c]); + } + + // reset the data pointer + buf.reset(); + + // update the offset + glyphArrayOffset += map.length * 2; + } else { + buf.putShort((short) 0); + } + } + + // make sure we are at the end of the buffer before we flip + buf.position(glyphArrayOffset); + + // reset the data pointer + buf.flip(); + + return buf; + } + + /** + * Get the segment count + */ + public short getSegmentCount() { + return (short) this.segments.size(); + } + + /** + * Get the search range + */ + public short getSearchRange() { + double pow = Math.floor(Math.log(getSegmentCount()) / Math.log(2)); + double pow2 = Math.pow(2, pow); + + return (short) (2 * pow2); + } + + /** + * Get the entry selector + */ + public short getEntrySelector() { + int sr2 = getSearchRange() / 2; + return (short) (Math.log(sr2) / Math.log(2)); + } + + /** + * Get the rangeShift() + */ + public short getRangeShift() { + return (short) ((2 * getSegmentCount()) - getSearchRange()); + } + + /** Get a pretty string */ + @Override public String toString() { + StringBuffer buf = new StringBuffer(); + String indent = " "; + + buf.append(super.toString()); + buf.append(indent + "SegmentCount : " + getSegmentCount() + "\n"); + buf.append(indent + "SearchRange : " + getSearchRange() + "\n"); + buf.append(indent + "EntrySelector: " + getEntrySelector() + "\n"); + buf.append(indent + "RangeShift : " + getRangeShift() + "\n"); + + for (Iterator i = this.segments.keySet().iterator(); i.hasNext();) { + Segment s = i.next(); + + buf.append(indent); + buf.append("Segment: " + Integer.toHexString(s.startCode)); + buf.append("-" + Integer.toHexString(s.endCode) + " "); + buf.append("hasMap: " + s.hasMap + " "); + + if (!s.hasMap) { + buf.append("delta: " + this.segments.get(s)); + } + + buf.append("\n"); + } + + return buf.toString(); + } + + static class Segment implements Comparable { + /** the end code (highest code in this segment) */ + int endCode; + + /** the start code (lowest code in this segment) */ + int startCode; + + /** whether it is a map or a delta */ + boolean hasMap; + + /** Create a new segment */ + public Segment(short startCode, short endCode, boolean hasMap) { + // convert from unsigned short + this.endCode = (0xffff & endCode); + this.startCode = (0xffff & startCode); + + this.hasMap = hasMap; + } + + /** Equals based on compareTo (only compares endCode) */ + @Override public boolean equals(Object o) { + return (compareTo(o) == 0); + } + + /** Segments sort by increasing endCode */ + @Override + public int compareTo(Object o) { + if (!(o instanceof Segment)) { + return -1; + } + + Segment s = (Segment) o; + + // if regions overlap at all, declare the segments equal, + // to avoid overlap in the segment list + if (((s.endCode >= this.startCode) && (s.endCode <= this.endCode)) || + ((s.startCode >= this.startCode) && (s.startCode <= this.endCode))) { + return 0; + } if (this.endCode > s.endCode) { + return 1; + } else if (this.endCode < s.endCode) { + return -1; + } else { + return 0; + } + } + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/CMapFormat6.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/CMapFormat6.java new file mode 100644 index 0000000000..536ac4471c --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/CMapFormat6.java @@ -0,0 +1,140 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.font.ttf; + +import java.nio.ByteBuffer; +import java.util.*; + +/** + * + * @author jkaplan + */ +public class CMapFormat6 extends CMap { + /** First character code of subrange. */ + private short firstCode; + /** Number of character codes in subrange. */ + private short entryCount; + /** Array of glyph index values for character codes in the range. */ + private short [] glyphIndexArray; + /** a reverse lookup from glyph id to index. */ + private HashMap glyphLookup = new HashMap(); + + /** Creates a new instance of CMapFormat0 */ + protected CMapFormat6(short language) { + super((short) 6, language); + } + + /** + * Get the length of this table + */ + @Override + public short getLength() { + // start with the size of the fixed header + short size = 5 * 2; + + // add the size of each segment header + size += this.entryCount * 2; + return size; + } + + /** + * Cannot map from a byte + */ + @Override + public byte map(byte src) { + char c = map((char) src); + if (c < Byte.MIN_VALUE || c > Byte.MAX_VALUE) { + // out of range + return 0; + } + return (byte) c; + } + + /** + * Map from char + */ + @Override + public char map(char src) { + + // find first segment with endcode > src + if (src < this.firstCode || src > (this.firstCode + this.entryCount)) { + // Codes outside of the range are assumed to be missing and are + // mapped to the glyph with index 0 + return '\000'; + } + return (char) this.glyphIndexArray[src - this.firstCode]; + } + + /** + * Get the src code which maps to the given glyphID + */ + @Override + public char reverseMap(short glyphID) { + Short result = this.glyphLookup.get(Short.valueOf(glyphID)); + if (result == null) { + return '\000'; + } + return (char) result.shortValue(); + } + + + /** + * Get the data in this map as a ByteBuffer + */ + @Override + public void setData(int length, ByteBuffer data) { + // read the table size values + this.firstCode = data.getShort(); + this.entryCount = data.getShort(); + + this.glyphIndexArray = new short [this.entryCount]; + for (int i = 0; i < this.glyphIndexArray.length; i++) { + this.glyphIndexArray[i] = data.getShort(); + this.glyphLookup.put(Short.valueOf(this.glyphIndexArray[i]), + Short.valueOf((short) (i + this.firstCode))); + } + } + + /** + * Get the data in the map as a byte buffer + */ + @Override + public ByteBuffer getData() { + ByteBuffer buf = ByteBuffer.allocate(getLength()); + + // write the header + buf.putShort(getFormat()); + buf.putShort(getLength()); + buf.putShort(getLanguage()); + + // write the various values + buf.putShort(this.firstCode); + buf.putShort(this.entryCount); + + // write the endCodes + for (int i = 0; i < this.glyphIndexArray.length; i++) { + buf.putShort(this.glyphIndexArray[i]); + } + // reset the data pointer + buf.flip(); + + return buf; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/CmapTable.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/CmapTable.java new file mode 100644 index 0000000000..1eae96953d --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/CmapTable.java @@ -0,0 +1,294 @@ +/* Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.font.ttf; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.SortedMap; +import java.util.TreeMap; + +import com.github.librepdf.pdfrenderer.PDFDebugger; + +/** + * Represents the TTF "cmap" table + * + * @author jkaplan + */ +public class CmapTable extends TrueTypeTable { + + /** Holds value of property version. */ + private short version; + + /** + * Holds the CMap subtables, sorted properly + */ + private final SortedMap subtables; + + /** Creates a new instance of CmapTable */ + protected CmapTable() { + super(TrueTypeTable.CMAP_TABLE); + + setVersion((short) 0x0); + + this.subtables = Collections.synchronizedSortedMap(new TreeMap()); + } + + /** + * Add a CMap + */ + public void addCMap(short platformID, short platformSpecificID, + CMap cMap) { + CmapSubtable key = new CmapSubtable(platformID, platformSpecificID); + this.subtables.put(key, cMap); + } + + /** + * Get a CMap by platform and specific ID + */ + public CMap getCMap(short platformID, short platformSpecificID) { + CmapSubtable key = new CmapSubtable(platformID, platformSpecificID); + return this.subtables.get(key); + } + + /** + * Get all CMaps + */ + public CMap[] getCMaps() { + Collection c = new ArrayList(); + + CMap cmap_3_1 = this.getCMap((short)3, (short)1); + if (cmap_3_1 != null) { + c.add(cmap_3_1); + } + CMap cmap_1_0 = this.getCMap((short)1, (short)0); + if (cmap_1_0 != null) { + c.add(cmap_1_0); + } + + for (CMap cmap : this.subtables.values()) { + if (!c.contains(cmap)) { + c.add(cmap); + } + } + ; + CMap[] maps = new CMap[c.size()]; + + c.toArray(maps); + + return maps; + } + + /** + * Remove a CMap + */ + public void removeCMap(short platformID, short platformSpecificID) { + CmapSubtable key = new CmapSubtable(platformID, platformSpecificID); + this.subtables.remove(key); + } + + @Override public void setData(ByteBuffer data) { + setVersion(data.getShort()); + + short numberSubtables = data.getShort(); + + for (int i = 0; i < numberSubtables; i++) { + short platformID = data.getShort(); + short platformSpecificID = data.getShort(); + int offset = data.getInt(); + + data.mark(); + + // get the position from the start of this buffer + data.position(offset); + + ByteBuffer mapData = data.slice(); + + data.reset(); + + try { + CMap cMap = CMap.getMap(mapData); + if (cMap != null) { + addCMap(platformID, platformSpecificID, cMap); + } + } catch (Exception ex) { + PDFDebugger.debug("Error reading map. PlatformID=" + + platformID + ", PlatformSpecificID=" + + platformSpecificID); + PDFDebugger.debug("Reason: " + ex); + } + } + } + + @Override public ByteBuffer getData() { + ByteBuffer buf = ByteBuffer.allocate(getLength()); + + // write the table header + buf.putShort(getVersion()); + buf.putShort((short) this.subtables.size()); + + // the current offset to write to, starts at the end of the + // subtables + int curOffset = 4 + (this.subtables.size() * 8); + + // write the subtables + for (Iterator i = this.subtables.keySet().iterator(); i.hasNext();) { + CmapSubtable cms = (CmapSubtable) i.next(); + CMap map = this.subtables.get(cms); + + buf.putShort(cms.platformID); + buf.putShort(cms.platformSpecificID); + buf.putInt(curOffset); + + curOffset += map.getLength(); + } + + // write the tables + for (Iterator i = this.subtables.values().iterator(); i.hasNext();) { + CMap map = (CMap) i.next(); + buf.put(map.getData()); + } + + // reset the position to the start of the buffer + buf.flip(); + + return buf; + } + + /** + * Get the size of the table, in bytes + */ + @Override public int getLength() { + // start with the size of the fixed data + int length = 4; + + // add the size of the subtables + length += this.subtables.size() * 8; + + // add the size of the dynamic data + for (Iterator i = this.subtables.values().iterator(); i.hasNext();) { + // add the size of the subtable data + CMap map = (CMap) i.next(); + length += map.getLength(); + } + + return length; + } + + + /** Getter for property version. + * @return Value of property version. + * + */ + public short getVersion() { + return this.version; + } + + /** Setter for property version. + * @param version New value of property version. + * + */ + public void setVersion(short version) { + this.version = version; + } + + /** + * Get the number of tables + */ + public short getNumberSubtables() { + return (short) this.subtables.size(); + } + + /** Print a pretty string */ + @Override public String toString() { + StringBuffer buf = new StringBuffer(); + String indent = " "; + + buf.append(indent + "Version: " + this.getVersion() + "\n"); + buf.append(indent + "NumMaps: " + this.getNumberSubtables() + "\n"); + + for (Iterator i = this.subtables.keySet().iterator(); i.hasNext();) { + CmapSubtable key = (CmapSubtable) i.next(); + + buf.append(indent + "Map: platformID: " + key.platformID + + " PlatformSpecificID: " + key.platformSpecificID + "\n"); + + CMap map = this.subtables.get(key); + + buf.append(map.toString()); + } + + return buf.toString(); + } + + static class CmapSubtable implements Comparable { + /** + * The platformID for this subtable + */ + short platformID; + + /** + * The platform-specific id + */ + short platformSpecificID; + + /** + * Create a Cmap subtable + */ + protected CmapSubtable(short platformID, short platformSpecificID) { + this.platformID = platformID; + this.platformSpecificID = platformSpecificID; + } + + /** + * Compare two subtables + */ + @Override public boolean equals(Object obj) { + return (compareTo(obj) == 0); + } + + /** + * Sort ascending by platform ID and then specific ID + */ + @Override + public int compareTo(Object obj) { + if (!(obj instanceof CmapSubtable)) { + return -1; + } + + CmapSubtable cms = (CmapSubtable) obj; + if (this.platformID < cms.platformID) { + return -1; + } else if (this.platformID > cms.platformID) { + return 1; + } else { + if (this.platformSpecificID < cms.platformSpecificID) { + return -1; + } else if (this.platformSpecificID > cms.platformSpecificID) { + return 1; + } else { + return 0; + } + } + } + } + +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/Glyf.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/Glyf.java new file mode 100644 index 0000000000..e0d6a33ed0 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/Glyf.java @@ -0,0 +1,206 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.font.ttf; + +import java.nio.ByteBuffer; + +/** + * A single glyph in a pdf font. May be simple or compound via subclasses + */ +public class Glyf { + /** If true, the glyf is compound */ + private boolean isCompound; + + /** the number of contours */ + private short numContours; + + /** the minimum x value */ + private short minX; + + /** the minimum y value */ + private short minY; + + /** the maximum x value */ + private short maxX; + + /** the maximum y value */ + private short maxY; + + /** + * Creates a new instance of glyf + * Don't use this directly, use Glyf.getGlyf() + */ + protected Glyf() { + } + + /** + * Get a map from the given data + * + * This method reads the format, data and length variables of + * the map. + */ + public static Glyf getGlyf(ByteBuffer data) { + short numContours = data.getShort(); + + Glyf g = null; + if (numContours == 0) { + // no glyph data + g = new Glyf(); + } else if (numContours == -1) { + // compound glyf + g = new GlyfCompound(); + } else if (numContours > 0) { + // simple glyf + g = new GlyfSimple(); + } else { + throw new IllegalArgumentException("Unknown glyf type: " + + numContours); + } + + g.setNumContours(numContours); + g.setMinX(data.getShort()); + g.setMinY(data.getShort()); + g.setMaxX(data.getShort()); + g.setMaxY(data.getShort()); + + // do glyphtype-specific parsing + g.setData(data); + + return g; + } + + /** + * Set the data for this glyf. Do nothing, since a glyf with + * no contours has no glyf data. + */ + public void setData(ByteBuffer data) { + return; + } + + /** + * Get the data in this glyf as a byte buffer. Return the basic + * glyf data only, since there is no specific data. This method returns + * the data un-flipped, so subclasses can simply append to the allocated + * buffer. + */ + public ByteBuffer getData() { + ByteBuffer buf = ByteBuffer.allocate(getLength()); + + buf.putShort(getNumContours()); + buf.putShort(getMinX()); + buf.putShort(getMinY()); + buf.putShort(getMaxX()); + buf.putShort(getMaxY()); + + // don't flip the buffer, since it may be used by subclasses + return buf; + } + + /** + * Get the length of this glyf. A glyf with no data has a length + * of 10 (2 bytes each for 5 short values) + */ + public short getLength() { + return 10; + } + + /** + * Get whether this is a simple or compound glyf + */ + public boolean isCompound() { + return this.isCompound; + } + + /** + * Set whether this is a simple or compound glyf + */ + protected void setCompound(boolean isCompound) { + this.isCompound = isCompound; + } + + /** + * Get the number of contours in this glyf + */ + public short getNumContours() { + return this.numContours; + } + + /** + * Set the number of contours in this glyf + */ + protected void setNumContours(short numContours) { + this.numContours = numContours; + } + + /** + * Get the minimum x in this glyf + */ + public short getMinX() { + return this.minX; + } + + /** + * Set the minimum X in this glyf + */ + protected void setMinX(short minX) { + this.minX = minX; + } + + /** + * Get the minimum y in this glyf + */ + public short getMinY() { + return this.minY; + } + + /** + * Set the minimum Y in this glyf + */ + protected void setMinY(short minY) { + this.minY = minY; + } + /** + * Get the maximum x in this glyf + */ + public short getMaxX() { + return this.maxX; + } + + /** + * Set the maximum X in this glyf + */ + protected void setMaxX(short maxX) { + this.maxX = maxX; + } + + /** + * Get the maximum y in this glyf + */ + public short getMaxY() { + return this.maxY; + } + + /** + * Set the maximum Y in this glyf + */ + protected void setMaxY(short maxY) { + this.maxY = maxY; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/GlyfCompound.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/GlyfCompound.java new file mode 100644 index 0000000000..b4add4587f --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/GlyfCompound.java @@ -0,0 +1,329 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.font.ttf; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * A single simple glyph in a pdf font. + */ +public class GlyfCompound extends Glyf { + /** flags */ + private static final int ARG_1_AND_2_ARE_WORDS = 0x1; + private static final int ARGS_ARE_XY_VALUES = 0x2; + private static final int ROUND_XY_TO_GRID = 0x4; + private static final int WE_HAVE_A_SCALE = 0x8; + private static final int MORE_COMPONENTS = 0x20; + private static final int WE_HAVE_AN_X_AND_Y_SCALE = 0x40; + private static final int WE_HAVE_A_TWO_BY_TWO = 0x80; + private static final int WE_HAVE_INSTRUCTIONS = 0x100; + private static final int USE_MY_METRICS = 0x200; + private static final int OVERLAP_COMPOUND = 0x400; + + /** the flags for each compound glyph */ + private GlyfComponent[] components; + + /** the instructions for the compound as a whole */ + private byte[] instructions; + + /** + * Creates a new instance of a simple glyf + */ + protected GlyfCompound() { + } + + /** + * Set the data for this glyf. + */ + @Override public void setData(ByteBuffer data) { + // int pos = data.position(); + // byte[] prdata = new byte[data.remaining()]; + // data.get(prdata); + // HexDump.printData(prdata); + // data.position(pos); + + // read the contour end points + List comps = new ArrayList(); + GlyfComponent cur = null; + boolean hasInstructions = false; + + do { + cur = new GlyfComponent(); + cur.flags = data.getShort(); + cur.glyphIndex = data.getShort() & 0xFFFF; + + // read either e/f or matching points, as shorts or bytes... + if (((cur.flags & ARG_1_AND_2_ARE_WORDS) != 0) && + ((cur.flags & ARGS_ARE_XY_VALUES) != 0)) { + cur.e = data.getShort(); + cur.f = data.getShort(); + } else if (!((cur.flags & ARG_1_AND_2_ARE_WORDS) != 0) && + ((cur.flags & ARGS_ARE_XY_VALUES) != 0)) { + cur.e = data.get(); + cur.f = data.get(); + } else if ( ((cur.flags & ARG_1_AND_2_ARE_WORDS) != 0) && + !((cur.flags & ARGS_ARE_XY_VALUES) != 0)) { + cur.compoundPoint = data.getShort(); + cur.componentPoint = data.getShort(); + } else { + cur.compoundPoint = data.get(); + cur.componentPoint = data.get(); + } + + // read the linear transform + if ((cur.flags & WE_HAVE_A_SCALE) != 0) { + cur.a = (float) data.getShort() / (float) (1 << 14); + cur.d = cur.a; + } else if ((cur.flags & WE_HAVE_AN_X_AND_Y_SCALE) != 0) { + cur.a = (float) data.getShort() / (float) (1 << 14); + cur.d = (float) data.getShort() / (float) (1 << 14); + } else if ((cur.flags & WE_HAVE_A_TWO_BY_TWO) != 0) { + cur.a = (float) data.getShort() / (float) (1 << 14); + cur.b = (float) data.getShort() / (float) (1 << 14); + cur.c = (float) data.getShort() / (float) (1 << 14); + cur.d = (float) data.getShort() / (float) (1 << 14); + } + + if ((cur.flags & WE_HAVE_INSTRUCTIONS) != 0) { + hasInstructions = true; + } + + comps.add(cur); + } while ((cur.flags & MORE_COMPONENTS) != 0); + + GlyfComponent[] componentArray = new GlyfComponent[comps.size()]; + comps.toArray(componentArray); + setComponents(componentArray); + + byte[] instr = null; + if (hasInstructions) { + // read the instructions + short numInstructions = data.getShort(); + instr = new byte[numInstructions]; + for (int i = 0; i < instr.length; i++) { + instr[i] = data.get(); + } + } else { + instr = new byte[0]; + } + setInstructions(instr); + } + + /** + * Get the data in this glyf as a byte buffer. Not implemented. + */ + @Override public ByteBuffer getData() { + ByteBuffer buf = super.getData(); + + // don't flip the buffer, since it may be used by subclasses + return buf; + } + + /** + * Get the length of this glyf. Not implemented. + */ + @Override public short getLength() { + + // start with the length of the superclass + short length = super.getLength(); + return length; + } + + /** + * Get the number of components in this compound + */ + public int getNumComponents() { + return this.components.length; + } + + /** + * Get a given flag + */ + public short getFlag(int index) { + return this.components[index].flags; + } + + /** + * Get the glyf index for a given glyf + */ + public int getGlyphIndex(int index) { + return this.components[index].glyphIndex; + } + + /** + * Get the base affine transform. This is based on a whacy formula + * defined in the true type font spec. + */ + public double[] getTransform(int index) { + GlyfComponent gc = this.components[index]; + + float m = Math.max(Math.abs(gc.a), Math.abs(gc.b)); + if (Math.abs(Math.abs(gc.a) - Math.abs(gc.c)) < (33 / 65536)) { + m *= 2; + } + + float n = Math.max(Math.abs(gc.c), Math.abs(gc.d)); + if (Math.abs(Math.abs(gc.c) - Math.abs(gc.d)) < (33 / 65536)) { + n *= 2; + } + + float e = m * gc.e; + float f = n * gc.f; + + return new double[] { gc.a, gc.b, gc.c, gc.d, e, f }; + } + + /** + * Get the point in the compound glyph to match + */ + public int getCompoundPoint(int index) { + return this.components[index].compoundPoint; + } + + /** + * Get the point in the component glyph to match + */ + public int getComponentPoint(int index) { + return this.components[index].componentPoint; + } + + /** + * Determine whether args 1 and 2 are words or bytes + */ + public boolean argsAreWords(int index) { + return ((getFlag(index) & ARG_1_AND_2_ARE_WORDS) != 0); + } + + /** + * Determine whether args 1 and 2 are xy values or point indices + */ + public boolean argsAreXYValues(int index) { + return ((getFlag(index) & ARGS_ARE_XY_VALUES) != 0); + } + + /** + * Determine whether to round XY values to the grid + */ + public boolean roundXYToGrid(int index) { + return ((getFlag(index) & ROUND_XY_TO_GRID) != 0); + } + + /** + * Determine whether there is a simple scale + */ + public boolean hasAScale(int index) { + return ((getFlag(index) & WE_HAVE_A_SCALE) != 0); + } + + /** + * Determine whether there are more components left to read + */ + protected boolean moreComponents(int index) { + return ((getFlag(index) & MORE_COMPONENTS) != 0); + } + + /** + * Determine whether there are separate scales on X and Y + */ + protected boolean hasXYScale(int index) { + return ((getFlag(index) & WE_HAVE_AN_X_AND_Y_SCALE) != 0); + } + + /** + * Determine whether there is a 2x2 transform + */ + protected boolean hasTwoByTwo(int index) { + return ((getFlag(index) & WE_HAVE_A_TWO_BY_TWO) != 0); + } + + /** + * Determine whether there are instructions + */ + protected boolean hasInstructions(int index) { + return ((getFlag(index) & WE_HAVE_INSTRUCTIONS) != 0); + } + + /** + * Use the metrics of this component for the compound + */ + public boolean useMetrics(int index) { + return ((getFlag(index) & USE_MY_METRICS) != 0); + } + + /** + * This component overlaps the existing compound + */ + public boolean overlapCompound(int index) { + return ((getFlag(index) & OVERLAP_COMPOUND) != 0); + } + + /** + * Set the components + */ + void setComponents(GlyfComponent[] components) { + this.components = components; + } + + /** + * Get the number of instructions + */ + public short getNumInstructions() { + return (short) this.instructions.length; + } + + /** + * Get a given instruction + */ + public byte getInstruction(int index) { + return this.instructions[index]; + } + + /** + * Set the instructions + */ + protected void setInstructions(byte[] instructions) { + this.instructions = instructions; + } + + /** + * The record for a single component of this compound glyph + */ + static class GlyfComponent { + /** flags */ + short flags; + + /** the index of the component glyf */ + int glyphIndex; + + /** the points to match */ + int compoundPoint; + int componentPoint; + + /** affine transform of this component */ + float a = 1.0f; + float b = 0.0f; + float c = 0.0f; + float d = 1.0f; + float e = 0.0f; + float f = 0.0f; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/GlyfSimple.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/GlyfSimple.java new file mode 100644 index 0000000000..4947749595 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/GlyfSimple.java @@ -0,0 +1,364 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.font.ttf; + +import java.nio.ByteBuffer; + +/** + * A single simple glyph in a pdf font. + */ +public class GlyfSimple extends Glyf { + /** the end points of the various contours */ + private short[] contourEndPts; + + /** the instructions */ + private byte[] instructions; + + /** the flags */ + private byte[] flags; + + /** the x coordinates */ + private short[] xCoords; + + /** the y coordinates */ + private short[] yCoords; + + /** + * Creates a new instance of a simple glyf + */ + protected GlyfSimple() { + } + + /** + * Set the data for this glyf. + */ + @Override + public void setData(ByteBuffer data) { + // int pos = data.position(); + // byte[] prdata = new byte[data.remaining()]; + // data.get(prdata); + // HexDump.printData(prdata); + // data.position(pos); + + + // read the contour end points + short[] contourEndPts = new short[getNumContours()]; + for (int i = 0; i < contourEndPts.length; i++) { + contourEndPts[i] = data.getShort(); + } + setContourEndPoints(contourEndPts); + + // the number of points in the glyf is the number of the end + // point in the last contour + int numPoints = getContourEndPoint(getNumContours() - 1) + 1; + + // read the instructions + short numInstructions = data.getShort(); + byte[] instructions = new byte[numInstructions]; + for (int i = 0; i < instructions.length; i++) { + instructions[i] = data.get(); + } + setInstructions(instructions); + + // read the flags + byte[] flags = new byte[numPoints]; + for (int i = 0; i < flags.length; i++) { + flags[i] = data.get(); + + // check for repeats + if ((flags[i] & 0x8) != 0) { + byte f = flags[i]; + int n = (data.get() & 0xff); + for (int c = 0; c < n; c++) { + flags[++i] = f; + } + } + } + setFlags(flags); + + // read the x coordinates + short[] xCoords = new short[numPoints]; + for (int i = 0; i < xCoords.length; i++) { + if (i > 0) { + xCoords[i] = xCoords[i - 1]; + } + + // read this value + if (xIsByte(i)) { + int val = (data.get() & 0xff); + if (!xIsSame(i)) { + // the xIsSame bit controls the sign + val = -val; + } + xCoords[i] += val; + } else if (!xIsSame(i)) { + xCoords[i] += data.getShort(); + } + } + setXCoords(xCoords); + + // read the y coordinates + short[] yCoords = new short[numPoints]; + for (int i = 0; i < yCoords.length; i++) { + if (i > 0) { + yCoords[i] = yCoords[i - 1]; + } + // read this value + if (yIsByte(i)) { + int val = (data.get() & 0xff); + if (!yIsSame(i)) { + // the xIsSame bit controls the sign + val = -val; + } + yCoords[i] += val; + } else if (!yIsSame(i)) { + yCoords[i] += data.getShort(); + } + } + setYCoords(yCoords); + } + + /** + * Get the data in this glyf as a byte buffer. Return the basic + * glyf data only, since there is no specific data. This method returns + * the data un-flipped, so subclasses can simply append to the allocated + * buffer. + */ + @Override + public ByteBuffer getData() { + ByteBuffer buf = super.getData(); + + // write the contour end points + for (int i = 0; i < getNumContours(); i++) { + buf.putShort(getContourEndPoint(i)); + } + + // write the instructions + buf.putShort(getNumInstructions()); + for (int i = 0; i < getNumInstructions(); i++) { + buf.put(getInstruction(i)); + } + + // write the flags + for (int i = 0; i < getNumPoints(); i++) { + // check for repeats + byte r = 0; + while (i > 0 && (getFlag(i) == getFlag(i - 1))) { + r++; + i++; + } + if (r > 0) { + buf.put(r); + } else { + buf.put(getFlag(i)); + } + } + + // write the x coordinates + for (int i = 0; i < getNumPoints(); i++) { + if (xIsByte(i)) { + buf.put((byte) getXCoord(i)); + } else if (!xIsSame(i)) { + buf.putShort(getXCoord(i)); + } + } + + // write the y coordinates + for (int i = 0; i < getNumPoints(); i++) { + if (yIsByte(i)) { + buf.put((byte) getYCoord(i)); + } else if (!yIsSame(i)) { + buf.putShort(getYCoord(i)); + } + } + + // don't flip the buffer, since it may be used by subclasses + return buf; + } + + /** + * Get the length of this glyf. + */ + @Override + public short getLength() { + // start with the length of the superclass + short length = super.getLength(); + + // add the length of the end points + length += getNumContours() * 2; + + // add the length of the instructions + length += 2 + getNumInstructions(); + + // add the length of the flags, avoiding repeats + for (int i = 0; i < getNumPoints(); i++) { + // check for repeats + while (i > 0 && (getFlag(i) == getFlag(i - 1))); + length++; + } + + // add the length of the xCoordinates + for (int i = 0; i < getNumPoints(); i++) { + if (xIsByte(i)) { + length++; + } else if (!xIsSame(i)) { + length += 2; + } + + if (yIsByte(i)) { + length++; + } else if (!yIsSame(i)) { + length += 2; + } + } + + return length; + } + + /** + * Get the end point of a given contour + */ + public short getContourEndPoint(int index) { + return this.contourEndPts[index]; + } + + /** + * Set the number of contours in this glyf + */ + protected void setContourEndPoints(short[] contourEndPts) { + this.contourEndPts = contourEndPts; + } + + /** + * Get the number of instructions + */ + public short getNumInstructions() { + return (short) this.instructions.length; + } + + /** + * Get a given instruction + */ + public byte getInstruction(int index) { + return this.instructions[index]; + } + + /** + * Set the instructions + */ + protected void setInstructions(byte[] instructions) { + this.instructions = instructions; + } + + /** + * Get the number of points in the glyf + */ + public short getNumPoints() { + return (short) this.flags.length; + } + + /** + * Get a given flag + */ + public byte getFlag(int pointIndex) { + return this.flags[pointIndex]; + } + + /** + * Determine whether the given point is on the curve + */ + public boolean onCurve(int pointIndex) { + return ((getFlag(pointIndex) & 0x1) != 0); + } + + /** + * Determine whether the x value for the given point is byte or short. + * If true, it is a byte, if false it is a short + */ + protected boolean xIsByte(int pointIndex) { + return ((getFlag(pointIndex) & 0x2) != 0); + } + + /** + * Determine whether the x value for the given point is byte or short. + * If true, it is a byte, if false it is a short + */ + protected boolean yIsByte(int pointIndex) { + return ((getFlag(pointIndex) & 0x4) != 0); + } + + /** + * Determine whether this flag repeats + */ + protected boolean repeat(int pointIndex) { + return ((getFlag(pointIndex) & 0x8) != 0); + } + + /** + * Determine whether the x value for the given point is the same as + * the previous value. + */ + protected boolean xIsSame(int pointIndex) { + return ((getFlag(pointIndex) & 0x10) != 0); + } + + /** + * Determine whether the y value for the given point is the same as + * the previous value. + */ + protected boolean yIsSame(int pointIndex) { + return ((getFlag(pointIndex) & 0x20) != 0); + } + + /** + * Set the flags + */ + protected void setFlags(byte[] flags) { + this.flags = flags; + } + + /** + * Get a given x coordinate + */ + public short getXCoord(int pointIndex) { + return this.xCoords[pointIndex]; + } + + /** + * Set the x coordinates + */ + protected void setXCoords(short[] xCoords) { + this.xCoords = xCoords; + } + + /** + * Get a given y coordinate + */ + public short getYCoord(int pointIndex) { + return this.yCoords[pointIndex]; + } + + /** + * Set the x coordinates + */ + protected void setYCoords(short[] yCoords) { + this.yCoords = yCoords; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/GlyfTable.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/GlyfTable.java new file mode 100644 index 0000000000..ee97f7cc50 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/GlyfTable.java @@ -0,0 +1,157 @@ +/* Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.font.ttf; + +import java.nio.ByteBuffer; + +/** + * Model the TrueType Glyf table + */ +public class GlyfTable extends TrueTypeTable { + /** + * the glyph data, as either a byte buffer (unparsed) or a + * glyph object (parsed) + */ + private Object[] glyphs; + + /** + * The glyph location table + */ + private LocaTable loca; + + /** Creates a new instance of HmtxTable */ + protected GlyfTable(TrueTypeFont ttf) { + super (TrueTypeTable.GLYF_TABLE); + + this.loca = (LocaTable) ttf.getTable("loca"); + + MaxpTable maxp = (MaxpTable) ttf.getTable("maxp"); + int numGlyphs = maxp.getNumGlyphs(); + + this.glyphs = new Object[numGlyphs]; + } + + /** + * Get the glyph at a given index, parsing it as needed + */ + public Glyf getGlyph(int index) { + Object o = this.glyphs[index]; + if (o == null) { + return null; + } + + if (o instanceof ByteBuffer) { + Glyf g = Glyf.getGlyf((ByteBuffer) o); + this.glyphs[index] = g; + + return g; + } else { + return (Glyf) o; + } + } + + /** get the data in this map as a ByteBuffer */ + @Override + public ByteBuffer getData() { + int size = getLength(); + + ByteBuffer buf = ByteBuffer.allocate(size); + + // write the offsets + for (int i = 0; i < this.glyphs.length; i++) { + Object o = this.glyphs[i]; + if (o == null) { + continue; + } + + ByteBuffer glyfData = null; + if (o instanceof ByteBuffer) { + glyfData = (ByteBuffer) o; + } else { + glyfData = ((Glyf) o).getData(); + } + + glyfData.rewind(); + buf.put(glyfData); + glyfData.flip(); + } + + // reset the start pointer + buf.flip(); + + return buf; + } + + /** Initialize this structure from a ByteBuffer */ + @Override + public void setData(ByteBuffer data) { + for (int i = 0; i < this.glyphs.length; i++) { + int location = this.loca.getOffset(i); + int length = this.loca.getSize(i); + + if (length == 0) { + // undefined glyph + continue; + } + + data.position(location); + ByteBuffer glyfData = data.slice(); + glyfData.limit(length); + + this.glyphs[i] = glyfData; + } + } + + /** + * Get the length of this table + */ + @Override + public int getLength() { + int length = 0; + + for (int i = 0; i < this.glyphs.length; i++) { + Object o = this.glyphs[i]; + if (o == null) { + continue; + } + + if (o instanceof ByteBuffer) { + length += ((ByteBuffer) o).remaining(); + } else { + length += ((Glyf) o).getLength(); + } + } + + return length; + } + + /** + * Create a pretty String + */ + @Override + public String toString() { + StringBuffer buf = new StringBuffer(); + String indent = " "; + + buf.append(indent + "Glyf Table: (" + this.glyphs.length + " glyphs)\n"); + buf.append(indent + " Glyf 0: " + getGlyph(0)); + + return buf.toString(); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/HeadTable.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/HeadTable.java new file mode 100644 index 0000000000..e8d66eb26c --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/HeadTable.java @@ -0,0 +1,473 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.font.ttf; + +import java.nio.ByteBuffer; +import java.util.Date; + +/** + * + * @author jkaplan + */ +public class HeadTable extends TrueTypeTable { + + /** Holds value of property version. */ + private int version; + + /** Holds value of property fontRevision. */ + private int fontRevision; + + /** Holds value of property checksumAdjustment. */ + private int checksumAdjustment; + + /** Holds value of property magicNumber. */ + private int magicNumber; + + /** Holds value of property flags. */ + private short flags; + + /** Holds value of property unitsPerEm. */ + private short unitsPerEm; + + /** Holds value of property created. */ + private long created; + + /** Holds value of property modified. */ + private long modified; + + /** Holds value of property xMin. */ + private short xMin; + + /** Holds value of property yMin. */ + private short yMin; + + /** Holds value of property xMax. */ + private short xMax; + + /** Holds value of property yMax. */ + private short yMax; + + /** Holds value of property macStyle. */ + private short macStyle; + + /** Holds value of property lowestRecPPem. */ + private short lowestRecPPem; + + /** Holds value of property fontDirectionHint. */ + private short fontDirectionHint; + + /** Holds value of property indexToLocFormat. */ + private short indexToLocFormat; + + /** Holds value of property glyphDataFormat. */ + private short glyphDataFormat; + + /** Creates a new instance of HeadTable + * Makes up reasonable(?) defaults for all values + */ + protected HeadTable() { + super(TrueTypeTable.HEAD_TABLE); + + setVersion(0x10000); + setFontRevision(0x10000); + setChecksumAdjustment(0); + setMagicNumber(0x5f0f3cf5); + setFlags((short) 0x0); + setUnitsPerEm((short) 64); + setCreated(System.currentTimeMillis()); + setModified(System.currentTimeMillis()); + setXMin((short) 0); + setXMax(Short.MAX_VALUE); + setYMin((short) 0); + setYMax(Short.MAX_VALUE); + setMacStyle((short) 0x0); + setLowestRecPPem((short) 0); + setFontDirectionHint((short) 0); + setIndexToLocFormat((short) 0); + setGlyphDataFormat((short) 0); + } + + /** + * Parse the data before it is set + */ + @Override + public void setData(ByteBuffer data) { + if (data.remaining() < 54) { + throw new IllegalArgumentException("Bad Head table size " + data.remaining()); + } + setVersion(data.getInt()); + setFontRevision(data.getInt()); + setChecksumAdjustment(data.getInt()); + setMagicNumber(data.getInt()); + setFlags(data.getShort()); + setUnitsPerEm(data.getShort()); + setCreated(data.getLong()); + setModified(data.getLong()); + setXMin(data.getShort()); + setXMax(data.getShort()); + setYMin(data.getShort()); + setYMax(data.getShort()); + setMacStyle(data.getShort()); + setLowestRecPPem(data.getShort()); + setFontDirectionHint(data.getShort()); + setIndexToLocFormat(data.getShort()); + setGlyphDataFormat(data.getShort()); + } + + /** + * Get the data we have stored + */ + @Override + public ByteBuffer getData() { + ByteBuffer buf = ByteBuffer.allocate(getLength()); + + buf.putInt(getVersion()); + buf.putInt(getFontRevision()); + buf.putInt(getChecksumAdjustment()); + buf.putInt(getMagicNumber()); + buf.putShort(getFlags()); + buf.putShort(getUnitsPerEm()); + buf.putLong(getCreated()); + buf.putLong(getModified()); + buf.putShort(getXMin()); + buf.putShort(getXMax()); + buf.putShort(getYMin()); + buf.putShort(getYMax()); + buf.putShort(getMacStyle()); + buf.putShort(getLowestRecPPem()); + buf.putShort(getFontDirectionHint()); + buf.putShort(getIndexToLocFormat()); + buf.putShort(getGlyphDataFormat()); + + // reset the position to the start of the buffer + buf.flip(); + + return buf; + } + + /** + * Get the length of this table + */ + @Override + public int getLength() { + return 54; + } + + /** Getter for property version. + * @return Value of property version. + * + */ + public int getVersion() { + return this.version; + } + + /** Getter for property fontRevision. + * @return Value of property fontRevision. + * + */ + public int getFontRevision() { + return this.fontRevision; + } + + /** Getter for property checksumAdjustment. + * @return Value of property checksumAdjustment. + * + */ + public int getChecksumAdjustment() { + return this.checksumAdjustment; + } + + /** Getter for property magicNumber. + * @return Value of property magicNumber. + * + */ + public int getMagicNumber() { + return this.magicNumber; + } + + /** Getter for property flags. + * @return Value of property flags. + * + */ + public short getFlags() { + return this.flags; + } + + /** Getter for property unitsPerEm. + * @return Value of property unitsPerEm. + * + */ + public short getUnitsPerEm() { + return this.unitsPerEm; + } + + /** Getter for property created. + * @return Value of property created. + * + */ + public long getCreated() { + return this.created; + } + + /** Getter for property modified. + * @return Value of property modified. + * + */ + public long getModified() { + return this.modified; + } + + /** Getter for property xMin. + * @return Value of property xMin. + * + */ + public short getXMin() { + return this.xMin; + } + + /** Getter for property yMin. + * @return Value of property yMin. + * + */ + public short getYMin() { + return this.yMin; + } + + /** Getter for property xMax. + * @return Value of property xMax. + * + */ + public short getXMax() { + return this.xMax; + } + + /** Getter for property yMax. + * @return Value of property yMax. + * + */ + public short getYMax() { + return this.yMax; + } + + /** Getter for property macStyle. + * @return Value of property macStyle. + * + */ + public short getMacStyle() { + return this.macStyle; + } + + /** Getter for property lowestRecPPem. + * @return Value of property lowestRecPPem. + * + */ + public short getLowestRecPPem() { + return this.lowestRecPPem; + } + + /** Getter for property fontDirectionHint. + * @return Value of property fontDirectionHint. + * + */ + public short getFontDirectionHint() { + return this.fontDirectionHint; + } + + /** Getter for property indexToLocFormat. + * @return Value of property indexToLocFormat. + * + */ + public short getIndexToLocFormat() { + return this.indexToLocFormat; + } + + /** Getter for property glyphDataFormat. + * @return Value of property glyphDataFormat. + * + */ + public short getGlyphDataFormat() { + return this.glyphDataFormat; + } + + /** Setter for property XMax. + * @param xMax New value of property XMax. + * + */ + public void setXMax(short xMax) { + this.xMax = xMax; + } + + /** Setter for property XMin. + * @param xMin New value of property XMin. + * + */ + public void setXMin(short xMin) { + this.xMin = xMin; + } + + /** Setter for property YMax. + * @param yMax New value of property YMax. + * + */ + public void setYMax(short yMax) { + this.yMax = yMax; + } + + /** Setter for property YMin. + * @param yMin New value of property YMin. + * + */ + public void setYMin(short yMin) { + this.yMin = yMin; + } + + /** Setter for property checksumAdjustment. + * @param checksumAdjustment New value of property checksumAdjustment. + * + */ + public void setChecksumAdjustment(int checksumAdjustment) { + this.checksumAdjustment = checksumAdjustment; + } + + /** Setter for property created. + * @param created New value of property created. + * + */ + public void setCreated(long created) { + this.created = created; + } + + /** Setter for property flags. + * @param flags New value of property flags. + * + */ + public void setFlags(short flags) { + this.flags = flags; + } + + /** Setter for property fontDirectionHint. + * @param fontDirectionHint New value of property fontDirectionHint. + * + */ + public void setFontDirectionHint(short fontDirectionHint) { + this.fontDirectionHint = fontDirectionHint; + } + + /** Setter for property fontRevision. + * @param fontRevision New value of property fontRevision. + * + */ + public void setFontRevision(int fontRevision) { + this.fontRevision = fontRevision; + } + + /** Setter for property glyphDataFormat. + * @param glyphDataFormat New value of property glyphDataFormat. + * + */ + public void setGlyphDataFormat(short glyphDataFormat) { + this.glyphDataFormat = glyphDataFormat; + } + + /** Setter for property indexToLocFormat. + * @param indexToLocFormat New value of property indexToLocFormat. + * + */ + public void setIndexToLocFormat(short indexToLocFormat) { + this.indexToLocFormat = indexToLocFormat; + } + + /** Setter for property lowestRecPPem. + * @param lowestRecPPem New value of property lowestRecPPem. + * + */ + public void setLowestRecPPem(short lowestRecPPem) { + this.lowestRecPPem = lowestRecPPem; + } + + /** Setter for property macStyle. + * @param macStyle New value of property macStyle. + * + */ + public void setMacStyle(short macStyle) { + this.macStyle = macStyle; + } + + /** Setter for property magicNumber. + * @param magicNumber New value of property magicNumber. + * + */ + public void setMagicNumber(int magicNumber) { + this.magicNumber = magicNumber; + } + + /** Setter for property modified. + * @param modified New value of property modified. + * + */ + public void setModified(long modified) { + this.modified = modified; + } + + /** Setter for property unitsPerEm. + * @param unitsPerEm New value of property unitsPerEm. + * + */ + public void setUnitsPerEm(short unitsPerEm) { + this.unitsPerEm = unitsPerEm; + } + + /** Setter for property version. + * @param version New value of property version. + * + */ + public void setVersion(int version) { + this.version = version; + } + + /** + * Create a pretty string + */ + @Override + public String toString() { + StringBuffer buf = new StringBuffer(); + String indent = " "; + + buf.append(indent + "Version : " + Integer.toHexString(getVersion()) + "\n"); + buf.append(indent + "Revision : " + Integer.toHexString(getFontRevision()) + "\n"); + buf.append(indent + "ChecksumAdj : " + Integer.toHexString(getChecksumAdjustment()) + "\n"); + buf.append(indent + "MagicNumber : " + Integer.toHexString(getMagicNumber()) + "\n"); + buf.append(indent + "Flags : " + Integer.toBinaryString(getFlags()) + "\n"); + buf.append(indent + "UnitsPerEm : " + getUnitsPerEm() + "\n"); + buf.append(indent + "Created : " + new Date(getCreated()) + "\n"); + buf.append(indent + "Modified : " + new Date(getModified()) + "\n"); + buf.append(indent + "XMin : " + getXMin() + "\n"); + buf.append(indent + "XMax : " + getXMax() + "\n"); + buf.append(indent + "YMin : " + getYMin() + "\n"); + buf.append(indent + "YMax : " + getYMax() + "\n"); + buf.append(indent + "MacStyle : " + Integer.toBinaryString(getMacStyle()) + "\n"); + buf.append(indent + "LowestPPem : " + getLowestRecPPem() + "\n"); + buf.append(indent + "FontDirectionHint: " + getFontDirectionHint() + "\n"); + buf.append(indent + "IndexToLocFormat : " + getIndexToLocFormat() + "\n"); + buf.append(indent + "GlyphDataFormat : " + getGlyphDataFormat() + "\n"); + + return buf.toString(); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/HheaTable.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/HheaTable.java new file mode 100644 index 0000000000..647cd027c4 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/HheaTable.java @@ -0,0 +1,382 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.font.ttf; + +import java.nio.ByteBuffer; + +/** + * + * @author jkaplan + */ +public class HheaTable extends TrueTypeTable { + + /** Holds value of property version. */ + private int version; + + /** Holds value of property ascent. */ + private short ascent; + + /** Holds value of property descent. */ + private short descent; + + /** Holds value of property lineGap. */ + private short lineGap; + + /** Holds value of property advanceWidthMax. */ + private short advanceWidthMax; + + /** Holds value of property minLeftSideBearing. */ + private short minLeftSideBearing; + + /** Holds value of property minRightSideBearing. */ + private short minRightSideBearing; + + /** Holds value of property xMaxExtent. */ + private short xMaxExtent; + + /** Holds value of property caretSlopeRise. */ + private short caretSlopeRise; + + /** Holds value of property caretSlopeRun. */ + private short caretSlopeRun; + + /** Holds value of property caretOffset. */ + private short caretOffset; + + /** Holds value of property metricDataFormat. */ + private short metricDataFormat; + + /** Holds value of property numOfLongHorMetrics. */ + private short numOfLongHorMetrics; + + /** Creates a new instance of HeadTable + * Makes up reasonable(?) defaults for all values + */ + protected HheaTable() { + super(TrueTypeTable.HEAD_TABLE); + + setVersion(0x10000); + } + + /** + * Parse the data before it is set + */ + @Override + public void setData(ByteBuffer data) { + if (data.remaining() != 36) { + throw new IllegalArgumentException("Bad Head table size"); + } + setVersion(data.getInt()); + setAscent(data.getShort()); + setDescent(data.getShort()); + setLineGap(data.getShort()); + setAdvanceWidthMax(data.getShort()); + setMinLeftSideBearing(data.getShort()); + setMinRightSideBearing(data.getShort()); + setXMaxExtent(data.getShort()); + setCaretSlopeRise(data.getShort()); + setCaretSlopeRun(data.getShort()); + setCaretOffset(data.getShort()); + + // padding + data.getShort(); + data.getShort(); + data.getShort(); + data.getShort(); + + setMetricDataFormat(data.getShort()); + setNumOfLongHorMetrics(data.getShort()); + } + + /** + * Get the data we have stored + */ + @Override + public ByteBuffer getData() { + ByteBuffer buf = ByteBuffer.allocate(getLength()); + + buf.putInt(getVersion()); + buf.putShort(getAscent()); + buf.putShort(getDescent()); + buf.putShort(getLineGap()); + buf.putShort(getAdvanceWidthMax()); + buf.putShort(getMinLeftSideBearing()); + buf.putShort(getMinRightSideBearing()); + buf.putShort(getXMaxExtent()); + buf.putShort(getCaretSlopeRise()); + buf.putShort(getCaretSlopeRun()); + buf.putShort(getCaretOffset()); + + // padding + buf.putShort((short) 0); + buf.putShort((short) 0); + buf.putShort((short) 0); + buf.putShort((short) 0); + + buf.putShort(getMetricDataFormat()); + buf.putShort((short) getNumOfLongHorMetrics()); + + // reset the position to the start of the buffer + buf.flip(); + + return buf; + } + + /** + * Get the length of this table + */ + @Override + public int getLength() { + return 36; + } + + /** Getter for property version. + * @return Value of property version. + * + */ + public int getVersion() { + return this.version; + } + + /** Setter for property version. + * @param version New value of property version. + * + */ + public void setVersion(int version) { + this.version = version; + } + + /** + * Create a pretty string + */ + @Override + public String toString() { + StringBuffer buf = new StringBuffer(); + String indent = " "; + + buf.append(indent + "Version : " + Integer.toHexString(getVersion()) + "\n"); + buf.append(indent + "Ascent : " + getAscent() + "\n"); + buf.append(indent + "Descent : " + getDescent() + "\n"); + buf.append(indent + "LineGap : " + getLineGap() + "\n"); + buf.append(indent + "AdvanceWidthMax : " + getAdvanceWidthMax() + "\n"); + buf.append(indent + "MinLSB : " + getMinLeftSideBearing() + "\n"); + buf.append(indent + "MinRSB : " + getMinRightSideBearing() + "\n"); + buf.append(indent + "MaxExtent : " + getXMaxExtent() + "\n"); + buf.append(indent + "CaretSlopeRise : " + getCaretSlopeRise() + "\n"); + buf.append(indent + "CaretSlopeRun : " + getCaretSlopeRun() + "\n"); + buf.append(indent + "CaretOffset : " + getCaretOffset() + "\n"); + buf.append(indent + "MetricDataFormat : " + getMetricDataFormat() + "\n"); + buf.append(indent + "NumOfLongHorMetrics : " + getNumOfLongHorMetrics() + "\n"); + return buf.toString(); + } + + /** Getter for property ascent. + * @return Value of property ascent. + * + */ + public short getAscent() { + return this.ascent; + } + + /** Setter for property ascent. + * @param ascent New value of property ascent. + * + */ + public void setAscent(short ascent) { + this.ascent = ascent; + } + + /** Getter for property descent. + * @return Value of property descent. + * + */ + public short getDescent() { + return this.descent; + } + + /** Setter for property descent. + * @param descent New value of property descent. + * + */ + public void setDescent(short descent) { + this.descent = descent; + } + + /** Getter for property lineGap. + * @return Value of property lineGap. + * + */ + public short getLineGap() { + return this.lineGap; + } + + /** Setter for property lineGap. + * @param lineGap New value of property lineGap. + * + */ + public void setLineGap(short lineGap) { + this.lineGap = lineGap; + } + + /** Getter for property advanceWidthMax. + * @return Value of property advanceWidthMax. + * + */ + public short getAdvanceWidthMax() { + return this.advanceWidthMax; + } + + /** Setter for property advanceWidthMax. + * @param advanceWidthMax New value of property advanceWidthMax. + * + */ + public void setAdvanceWidthMax(short advanceWidthMax) { + this.advanceWidthMax = advanceWidthMax; + } + + /** Getter for property minLeftSideBearing. + * @return Value of property minLeftSideBearing. + * + */ + public short getMinLeftSideBearing() { + return this.minLeftSideBearing; + } + + /** Setter for property minLeftSideBearing. + * @param minLeftSideBearing New value of property minLeftSideBearing. + * + */ + public void setMinLeftSideBearing(short minLeftSideBearing) { + this.minLeftSideBearing = minLeftSideBearing; + } + + /** Getter for property minRIghtSideBearing. + * @return Value of property minRIghtSideBearing. + * + */ + public short getMinRightSideBearing() { + return this.minRightSideBearing; + } + + /** Setter for property minRIghtSideBearing. + * @param minRightSideBearing New value of property minRIghtSideBearing. + * + */ + public void setMinRightSideBearing(short minRightSideBearing) { + this.minRightSideBearing = minRightSideBearing; + } + + /** Getter for property xMaxExtent. + * @return Value of property xMaxExtent. + * + */ + public short getXMaxExtent() { + return this.xMaxExtent; + } + + /** Setter for property xMaxExtent. + * @param xMaxExtent New value of property xMaxExtent. + * + */ + public void setXMaxExtent(short xMaxExtent) { + this.xMaxExtent = xMaxExtent; + } + + /** Getter for property caretSlopeRise. + * @return Value of property caretSlopeRise. + * + */ + public short getCaretSlopeRise() { + return this.caretSlopeRise; + } + + /** Setter for property caretSlopeRise. + * @param caretSlopeRise New value of property caretSlopeRise. + * + */ + public void setCaretSlopeRise(short caretSlopeRise) { + this.caretSlopeRise = caretSlopeRise; + } + + /** Getter for property caretSlopeRun. + * @return Value of property caretSlopeRun. + * + */ + public short getCaretSlopeRun() { + return this.caretSlopeRun; + } + + /** Setter for property caretSlopeRun. + * @param caretSlopeRun New value of property caretSlopeRun. + * + */ + public void setCaretSlopeRun(short caretSlopeRun) { + this.caretSlopeRun = caretSlopeRun; + } + + /** Getter for property caretOffset. + * @return Value of property caretOffset. + * + */ + public short getCaretOffset() { + return this.caretOffset; + } + + /** Setter for property caretOffset. + * @param caretOffset New value of property caretOffset. + * + */ + public void setCaretOffset(short caretOffset) { + this.caretOffset = caretOffset; + } + + /** Getter for property metricDataFormat. + * @return Value of property metricDataFormat. + * + */ + public short getMetricDataFormat() { + return this.metricDataFormat; + } + + /** Setter for property metricDataFormat. + * @param metricDataFormat New value of property metricDataFormat. + * + */ + public void setMetricDataFormat(short metricDataFormat) { + this.metricDataFormat = metricDataFormat; + } + + /** Getter for property numOfLongHorMetrics. + * @return Value of property numOfLongHorMetrics. + * + */ + public int getNumOfLongHorMetrics() { + return this.numOfLongHorMetrics & 0xFFFF; + } + + /** Setter for property numOfLongHorMetrics. + * @param numOfLongHorMetrics New value of property numOfLongHorMetrics. + * + */ + public void setNumOfLongHorMetrics(short numOfLongHorMetrics) { + this.numOfLongHorMetrics = numOfLongHorMetrics; + } + +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/HmtxTable.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/HmtxTable.java new file mode 100644 index 0000000000..49dc2ddde9 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/HmtxTable.java @@ -0,0 +1,122 @@ +/* Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.font.ttf; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** + * Model the TrueType Post table + * + * @author jkaplan + */ +public class HmtxTable extends TrueTypeTable { + /** advance widths for any glyphs that have one */ + short advanceWidths[]; + + /** left side bearings for each glyph */ + short leftSideBearings[]; + + /** Creates a new instance of HmtxTable */ + protected HmtxTable(TrueTypeFont ttf) { + super (TrueTypeTable.HMTX_TABLE); + + // the number of glyphs stored in the maxp table may be incorrect + // in the case of subsetted fonts produced by some pdf generators + MaxpTable maxp = (MaxpTable) ttf.getTable("maxp"); + int numGlyphs = maxp.getNumGlyphs(); + + HheaTable hhea = (HheaTable) ttf.getTable("hhea"); + int numOfLongHorMetrics = hhea.getNumOfLongHorMetrics(); + + this.advanceWidths = new short[numOfLongHorMetrics]; + this.leftSideBearings = new short[numGlyphs]; + } + + /** get the advance of a given glyph */ + public short getAdvance(int glyphID) { + if (glyphID < this.advanceWidths.length) { + return this.advanceWidths[glyphID]; + } else { + return this.advanceWidths[this.advanceWidths.length - 1]; + } + } + + /** get the left side bearing of a given glyph */ + public short getLeftSideBearing(int glyphID) { + return this.leftSideBearings[glyphID]; + } + + /** get the data in this map as a ByteBuffer */ + @Override + public ByteBuffer getData() { + int size = getLength(); + + ByteBuffer buf = ByteBuffer.allocate(size); + + // write the metrics + for (int i = 0; i < this.leftSideBearings.length; i++) { + if (i < this.advanceWidths.length) { + buf.putShort(this.advanceWidths[i]); + } + + buf.putShort(this.leftSideBearings[i]); + } + + // reset the start pointer + buf.flip(); + + return buf; + } + + /** Initialize this structure from a ByteBuffer */ + @Override + public void setData(ByteBuffer data) { + // some PDF writers subset the font but don't update the number of glyphs in the maxp table, + // this would appear to break the TTF spec. + // A better solution might be to try and override the numGlyphs in the maxp table based + // on the number of entries in the cmap table or by parsing the glyf table, but this + // appears to be the only place that gets affected by the discrepancy... so far!... + // so updating this allows it to work. + int i; + // only read as much data as is available + for (i = 0; i < this.leftSideBearings.length && data.hasRemaining(); i++) { + if (i < this.advanceWidths.length) { + this.advanceWidths[i] = data.getShort(); + } + + this.leftSideBearings[i] = data.getShort(); + } + // initialise the remaining advanceWidths and leftSideBearings to 0 + if (i < this.advanceWidths.length) { + Arrays.fill(this.advanceWidths, i, this.advanceWidths.length-1, (short) 0); + } + if (i < this.leftSideBearings.length) { + Arrays.fill(this.leftSideBearings, i, this.leftSideBearings.length-1, (short) 0); + } + } + + /** + * Get the length of this table + */ + @Override + public int getLength() { + return (this.advanceWidths.length * 2) + (this.leftSideBearings.length * 2); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/LocaTable.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/LocaTable.java new file mode 100644 index 0000000000..61b391002e --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/LocaTable.java @@ -0,0 +1,117 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.font.ttf; + +import java.nio.ByteBuffer; + +/** + * Model the TrueType Loca table + */ +public class LocaTable extends TrueTypeTable { + /** if true, the table stores glyphs in long format */ + private boolean isLong; + + /** the offsets themselves */ + private int offsets[]; + + /** Creates a new instance of HmtxTable */ + protected LocaTable(TrueTypeFont ttf) { + super (TrueTypeTable.LOCA_TABLE); + + MaxpTable maxp = (MaxpTable) ttf.getTable("maxp"); + int numGlyphs = maxp.getNumGlyphs(); + + HeadTable head = (HeadTable) ttf.getTable("head"); + short format = head.getIndexToLocFormat(); + this.isLong = (format == 1); + + this.offsets = new int[numGlyphs + 1]; + } + + /** + * get the offset, in bytes, of a given glyph from the start of + * the glyph table + */ + public int getOffset(int glyphID) { + return this.offsets[glyphID]; + } + + /** + * get the size, in bytes, of the given glyph + */ + public int getSize(int glyphID) { + return this.offsets[glyphID + 1] - this.offsets[glyphID]; + } + + /** + * Return true if the glyphs arte in long (int) format, or + * false if they are in short (short) format + */ + public boolean isLongFormat() { + return this.isLong; + } + + + /** get the data in this map as a ByteBuffer */ + @Override + public ByteBuffer getData() { + int size = getLength(); + + ByteBuffer buf = ByteBuffer.allocate(size); + + // write the offsets + for (int i = 0; i < this.offsets.length; i++) { + if (isLongFormat()) { + buf.putInt(this.offsets[i]); + } else { + buf.putShort((short) (this.offsets[i] / 2)); + } + } + + // reset the start pointer + buf.flip(); + + return buf; + } + + /** Initialize this structure from a ByteBuffer */ + @Override + public void setData(ByteBuffer data) { + for (int i = 0; i < this.offsets.length; i++) { + if (isLongFormat()) { + this.offsets[i] = data.getInt(); + } else { + this.offsets[i] = 2 * ( 0xFFFF & data.getShort()); + } + } + } + + /** + * Get the length of this table + */ + @Override + public int getLength() { + if (isLongFormat()) { + return this.offsets.length * 4; + } else { + return this.offsets.length * 2; + } + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/MaxpTable.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/MaxpTable.java new file mode 100644 index 0000000000..643c29dc60 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/MaxpTable.java @@ -0,0 +1,426 @@ +/* Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.font.ttf; + +import java.nio.ByteBuffer; + +/** + * + * @author jkaplan + */ +public class MaxpTable extends TrueTypeTable { + + /** Holds value of property version. */ + private int version; + + // the following are supposed to be USHORT, but will be Int to enclose the sign + // (http://www.microsoft.com/typography/OTSpec/maxp.htm) + /** Holds value of property numGlyphs. */ + private int numGlyphs; + + /** Holds value of property maxPoints. */ + private int maxPoints; + + /** Holds value of property maxContours. */ + private int maxContours; + + /** Holds value of property maxComponentPoints. */ + private int maxComponentPoints; + + /** Holds value of property maxComponentContours. */ + private int maxComponentContours; + + /** Holds value of property maxZones. */ + private int maxZones; + + /** Holds value of property maxTwilightPoints. */ + private int maxTwilightPoints; + + /** Holds value of property maxStorage. */ + private int maxStorage; + + /** Holds value of property maxFunctionDefs. */ + private int maxFunctionDefs; + + /** Holds value of property maxInstructionDefs. */ + private int maxInstructionDefs; + + /** Holds value of property maxStackElements. */ + private int maxStackElements; + + /** Holds value of property maxSizeOfInstructions. */ + private int maxSizeOfInstructions; + + /** Holds value of property maxComponentElements. */ + private int maxComponentElements; + + /** Holds value of property maxComponentDepth. */ + private int maxComponentDepth; + + /** Creates a new instance of MaxpTable */ + protected MaxpTable() { + super (TrueTypeTable.MAXP_TABLE); + + setVersion(0x10000); + setNumGlyphs(0); + setMaxPoints(0); + setMaxContours(0); + setMaxComponentPoints(0); + setMaxComponentContours(0); + setMaxZones(2); + setMaxTwilightPoints(0); + setMaxStorage(0); + setMaxFunctionDefs(0); + setMaxInstructionDefs(0); + setMaxStackElements(0); + setMaxSizeOfInstructions(0); + setMaxComponentElements(0); + setMaxComponentDepth(0); + } + + /** + * Set the values from data + */ + @Override + public void setData(ByteBuffer data) { + if (data.remaining() != 32) { + throw new IllegalArgumentException("Bad size for Maxp table"); + } + + setVersion(data.getInt()); + setNumGlyphs(data.getShort()); + setMaxPoints(data.getShort()); + setMaxContours(data.getShort()); + setMaxComponentPoints(data.getShort()); + setMaxComponentContours(data.getShort()); + setMaxZones(data.getShort()); + setMaxTwilightPoints(data.getShort()); + setMaxStorage(data.getShort()); + setMaxFunctionDefs(data.getShort()); + setMaxInstructionDefs(data.getShort()); + setMaxStackElements(data.getShort()); + setMaxSizeOfInstructions(data.getShort()); + setMaxComponentElements(data.getShort()); + setMaxComponentDepth(data.getShort()); + } + + /** + * Get a buffer from the data + */ + @Override + public ByteBuffer getData() { + ByteBuffer buf = ByteBuffer.allocate(getLength()); + + buf.putInt(getVersion()); + buf.putShort((short) getNumGlyphs()); + buf.putShort((short) getMaxPoints()); + buf.putShort((short) getMaxContours()); + buf.putShort((short) getMaxComponentPoints()); + buf.putShort((short) getMaxComponentContours()); + buf.putShort((short) getMaxZones()); + buf.putShort((short) getMaxTwilightPoints()); + buf.putShort((short) getMaxStorage()); + buf.putShort((short) getMaxFunctionDefs()); + buf.putShort((short) getMaxInstructionDefs()); + buf.putShort((short) getMaxStackElements()); + buf.putShort((short) getMaxSizeOfInstructions()); + buf.putShort((short) getMaxComponentElements()); + buf.putShort((short) getMaxComponentDepth()); + + // reset the position to the beginning of the buffer + buf.flip(); + + return buf; + } + + /** + * Get the length of this table + */ + @Override + public int getLength() { + return 32; + } + + /** Getter for property version. + * @return Value of property version. + * + */ + public int getVersion() { + return this.version; + } + + /** Setter for property version. + * @param version New value of property version. + * + */ + public void setVersion(int version) { + this.version = version; + } + + /** Getter for property numGlyphs. + * @return Value of property numGlyphs. + * + */ + public int getNumGlyphs() { + return this.numGlyphs & 0xFFFF; + } + + /** Setter for property numGlyphs. + * @param numGlyphs New value of property numGlyphs. + * + */ + public void setNumGlyphs(int numGlyphs) { + this.numGlyphs = numGlyphs; + } + + /** Getter for property maxPoints. + * @return Value of property maxPoints. + * + */ + public int getMaxPoints() { + return this.maxPoints & 0xFFFF; + } + + /** Setter for property maxPoints. + * @param maxPoints New value of property maxPoints. + * + */ + public void setMaxPoints(int maxPoints) { + this.maxPoints = maxPoints; + } + + /** Getter for property maxContours. + * @return Value of property maxContours. + * + */ + public int getMaxContours() { + return this.maxContours & 0xFFFF; + } + + /** Setter for property maxContours. + * @param maxContours New value of property maxContours. + * + */ + public void setMaxContours(int maxContours) { + this.maxContours = maxContours; + } + + /** Getter for property maxComponentPoints. + * @return Value of property maxComponentPoints. + * + */ + public int getMaxComponentPoints() { + return this.maxComponentPoints & 0xFFFF; + } + + /** Setter for property maxComponentPoints. + * @param maxComponentPoints New value of property maxComponentPoints. + * + */ + public void setMaxComponentPoints(int maxComponentPoints) { + this.maxComponentPoints = maxComponentPoints; + } + + /** Getter for property maxComponentContours. + * @return Value of property maxComponentContours. + * + */ + public int getMaxComponentContours() { + return this.maxComponentContours & 0xFFFF; + } + + /** Setter for property maxComponentContours. + * @param maxComponentContours New value of property maxComponentContours. + * + */ + public void setMaxComponentContours(int maxComponentContours) { + this.maxComponentContours = maxComponentContours; + } + + /** Getter for property maxZones. + * @return Value of property maxZones. + * + */ + public int getMaxZones() { + return this.maxZones & 0xFFFF; + } + + /** Setter for property maxZones. + * @param maxZones New value of property maxZones. + * + */ + public void setMaxZones(int maxZones) { + this.maxZones = maxZones; + } + + /** Getter for property maxTwilightPoints. + * @return Value of property maxTwilightPoints. + * + */ + public int getMaxTwilightPoints() { + return this.maxTwilightPoints & 0xFFFF; + } + + /** Setter for property maxTwilightPoints. + * @param maxTwilightPoints New value of property maxTwilightPoints. + * + */ + public void setMaxTwilightPoints(int maxTwilightPoints) { + this.maxTwilightPoints = maxTwilightPoints; + } + + /** Getter for property maxStorage. + * @return Value of property maxStorage. + * + */ + public int getMaxStorage() { + return this.maxStorage & 0xFFFF; + } + + /** Setter for property maxStorage. + * @param maxStorage New value of property maxStorage. + * + */ + public void setMaxStorage(int maxStorage) { + this.maxStorage = maxStorage; + } + + /** Getter for property maxFunctionDefs. + * @return Value of property maxFunctionDefs. + * + */ + public int getMaxFunctionDefs() { + return this.maxFunctionDefs & 0xFFFF; + } + + /** Setter for property maxFunctionDefs. + * @param maxFunctionDefs New value of property maxFunctionDefs. + * + */ + public void setMaxFunctionDefs(int maxFunctionDefs) { + this.maxFunctionDefs = maxFunctionDefs; + } + + /** Getter for property maxInstructionDefs. + * @return Value of property maxInstructionDefs. + * + */ + public int getMaxInstructionDefs() { + return this.maxInstructionDefs & 0xFFFF; + } + + /** Setter for property maxInstructionDefs. + * @param maxInstructionDefs New value of property maxInstructionDefs. + * + */ + public void setMaxInstructionDefs(int maxInstructionDefs) { + this.maxInstructionDefs = maxInstructionDefs; + } + + /** Getter for property maxStackElements. + * @return Value of property maxStackElements. + * + */ + public int getMaxStackElements() { + return this.maxStackElements & 0xFFFF; + } + + /** Setter for property maxStackElements. + * @param maxStackElements New value of property maxStackElements. + * + */ + public void setMaxStackElements(int maxStackElements) { + this.maxStackElements = maxStackElements; + } + + /** Getter for property maxSizeOfInstructions. + * @return Value of property maxSizeOfInstructions. + * + */ + public int getMaxSizeOfInstructions() { + return this.maxSizeOfInstructions & 0xFFFF; + } + + /** Setter for property maxSizeOfInstructions. + * @param maxSizeOfInstructions New value of property maxSizeOfInstructions. + * + */ + public void setMaxSizeOfInstructions(int maxSizeOfInstructions) { + this.maxSizeOfInstructions = maxSizeOfInstructions; + } + + /** Getter for property maxComponentElements. + * @return Value of property maxComponentElements. + * + */ + public int getMaxComponentElements() { + return this.maxComponentElements & 0xFFFF; + } + + /** Setter for property maxComponentElements. + * @param maxComponentElements New value of property maxComponentElements. + * + */ + public void setMaxComponentElements(int maxComponentElements) { + this.maxComponentElements = maxComponentElements; + } + + /** Getter for property maxComponentDepth. + * @return Value of property maxComponentDepth. + * + */ + public int getMaxComponentDepth() { + return this.maxComponentDepth & 0xFFFF; + } + + /** Setter for property maxComponentDepth. + * @param maxComponentDepth New value of property maxComponentDepth. + * + */ + public void setMaxComponentDepth(int maxComponentDepth) { + this.maxComponentDepth = maxComponentDepth; + } + + /** + * Create a pretty String + */ + @Override + public String toString() { + StringBuffer buf = new StringBuffer(); + String indent = " "; + + buf.append(indent + "Version : " + Integer.toHexString(getVersion()) + "\n"); + buf.append(indent + "NumGlyphs : " + getNumGlyphs() + "\n"); + buf.append(indent + "MaxPoints : " + getMaxPoints() + "\n"); + buf.append(indent + "MaxContours : " + getMaxContours() + "\n"); + buf.append(indent + "MaxCompPoints : " + getMaxComponentPoints() + "\n"); + buf.append(indent + "MaxCompContours : " + getMaxComponentContours() + "\n"); + buf.append(indent + "MaxZones : " + getMaxZones() + "\n"); + buf.append(indent + "MaxTwilightPoints: " + getMaxTwilightPoints() + "\n"); + buf.append(indent + "MaxStorage : " + getMaxStorage() + "\n"); + buf.append(indent + "MaxFuncDefs : " + getMaxFunctionDefs() + "\n"); + buf.append(indent + "MaxInstDefs : " + getMaxInstructionDefs() + "\n"); + buf.append(indent + "MaxStackElements : " + getMaxStackElements() + "\n"); + buf.append(indent + "MaxSizeInst : " + getMaxSizeOfInstructions() + "\n"); + buf.append(indent + "MaxCompElements : " + getMaxComponentElements() + "\n"); + buf.append(indent + "MaxCompDepth : " + getMaxComponentDepth() + "\n"); + + return buf.toString(); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/NameTable.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/NameTable.java new file mode 100644 index 0000000000..d152df4d93 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/NameTable.java @@ -0,0 +1,430 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.font.ttf; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * + * @author jon + */ +public class NameTable extends TrueTypeTable { + /** + * Values for platformID + */ + public static final short PLATFORMID_UNICODE = 0; + public static final short PLATFORMID_MACINTOSH = 1; + public static final short PLATFORMID_MICROSOFT = 3; + + /** + * Values for platformSpecificID if platform is Mac + */ + public static final short ENCODINGID_MAC_ROMAN = 0; + + /** + * Values for platformSpecificID if platform is Unicode + */ + public static final short ENCODINGID_UNICODE_DEFAULT = 0; + public static final short ENCODINGID_UNICODE_V11 = 1; + public static final short ENCODINGID_UNICODE_V2 = 3; + + /** + * Values for language ID if platform is Mac + */ + public static final short LANGUAGEID_MAC_ENGLISH = 0; + + /** + * Values for nameID + */ + public static final short NAMEID_COPYRIGHT = 0; + public static final short NAMEID_FAMILY = 1; + public static final short NAMEID_SUBFAMILY = 2; + public static final short NAMEID_SUBFAMILY_UNIQUE = 3; + public static final short NAMEID_FULL_NAME = 4; + public static final short NAMEID_VERSION = 5; + public static final short NAMEID_POSTSCRIPT_NAME = 6; + public static final short NAMEID_TRADEMARK = 7; + /** + * The format of this table + */ + private short format; + + /** + * The actual name records + */ + private SortedMap records; + + + /** Creates a new instance of NameTable */ + protected NameTable() { + super (TrueTypeTable.NAME_TABLE); + + this.records = Collections.synchronizedSortedMap(new TreeMap()); + } + + /** + * Add a record to the table + */ + public void addRecord(short platformID, short platformSpecificID, + short languageID, short nameID, + String value) { + NameRecord rec = new NameRecord(platformID, platformSpecificID, + languageID, nameID); + this.records.put(rec, value); + } + + /** + * Get a record from the table + */ + public String getRecord(short platformID, short platformSpecificID, + short languageID, short nameID) { + + NameRecord rec = new NameRecord(platformID, platformSpecificID, + languageID, nameID); + return this.records.get(rec); + } + + /** + * Remove a record from the table + */ + public void removeRecord(short platformID, short platformSpecificID, + short languageID, short nameID) { + NameRecord rec = new NameRecord(platformID, platformSpecificID, + languageID, nameID); + this.records.remove(rec); + } + + /** + * Determine if we have any records with a given platform ID + */ + public boolean hasRecords(short platformID) { + for (Iterator i = this.records.keySet().iterator(); i.hasNext(); ) { + NameRecord rec = (NameRecord) i.next(); + + if (rec.platformID == platformID) { + return true; + } + } + + return false; + } + + /** + * Determine if we have any records with a given platform ID and + * platform-specific ID + */ + public boolean hasRecords(short platformID, short platformSpecificID) { + for (Iterator i = this.records.keySet().iterator(); i.hasNext(); ) { + NameRecord rec = (NameRecord) i.next(); + + if (rec.platformID == platformID && + rec.platformSpecificID == platformSpecificID) { + return true; + } + } + + return false; + } + + /** + * Read the table from data + */ + @Override + public void setData(ByteBuffer data) { + //read table header + setFormat(data.getShort()); + int count = data.getShort(); + int stringOffset = data.getShort(); + + // read the records + for (int i = 0; i < count; i++) { + short platformID = data.getShort(); + short platformSpecificID = data.getShort(); + short languageID = data.getShort(); + short nameID = data.getShort(); + + int length = data.getShort() & 0xFFFF; + int offset = data.getShort() & 0xFFFF; + + // read the String data + data.mark(); + data.position(stringOffset + offset); + + ByteBuffer stringBuf = data.slice(); + stringBuf.limit(length); + + data.reset(); + + // choose the character set + String charsetName = getCharsetName(platformID, platformSpecificID); + Charset charset = Charset.forName(charsetName); + + // parse the data as a string + String value = charset.decode(stringBuf).toString(); + + // add to the mix + addRecord(platformID, platformSpecificID, languageID, nameID, value); + } + } + + /** + * Get the data in this table as a buffer + */ + @Override + public ByteBuffer getData() { + // alocate the output buffer + ByteBuffer buf = ByteBuffer.allocate(getLength()); + + // the start of string data + short headerLength = (short) (6 + (12 * getCount())); + + // write the header + buf.putShort(getFormat()); + buf.putShort(getCount()); + buf.putShort(headerLength); + + // the offset from the start of the strings table + short curOffset = 0; + + // add the size of each record + for (Iterator i = this.records.keySet().iterator(); i.hasNext();) { + NameRecord rec = (NameRecord) i.next(); + String value = this.records.get(rec); + + // choose the charset + String charsetName = getCharsetName(rec.platformID, + rec.platformSpecificID); + Charset charset = Charset.forName(charsetName); + + // encode + ByteBuffer strBuf = charset.encode(value); + short strLen = (short) (strBuf.remaining() & 0xFFFF); + + // write the IDs + buf.putShort(rec.platformID); + buf.putShort(rec.platformSpecificID); + buf.putShort(rec.languageID); + buf.putShort(rec.nameID); + + // write the size and offset + buf.putShort(strLen); + buf.putShort(curOffset); + + // remember or current position + buf.mark(); + + // move to the current offset and write the data + buf.position(headerLength + curOffset); + buf.put(strBuf); + + // reset stuff + buf.reset(); + + // increment offset + curOffset += strLen; + } + + // reset the pointer on the buffer + buf.position(headerLength + curOffset); + buf.flip(); + + return buf; + } + + /** + * Get the length of this table + */ + @Override + public int getLength() { + // start with the size of the fixed header plus the size of the + // records + int length = 6 + (12 * getCount()); + + // add the size of each record + for (Iterator i = this.records.keySet().iterator(); i.hasNext();) { + NameRecord rec = (NameRecord) i.next(); + String value = this.records.get(rec); + + // choose the charset + String charsetName = getCharsetName(rec.platformID, + rec.platformSpecificID); + Charset charset = Charset.forName(charsetName); + + // encode + ByteBuffer buf = charset.encode(value); + + // add the size of the coded buffer + length += buf.remaining(); + } + + return length; + } + + /** + * Get the format of this table + */ + public short getFormat() { + return this.format; + } + + /** + * Set the format of this table + */ + public void setFormat(short format) { + this.format = format; + } + + /** + * Get the number of records in the table + */ + public short getCount() { + return (short) this.records.size(); + } + + /** + * Get the charset name for a given platform, encoding and language + */ + public static String getCharsetName(int platformID, int encodingID) { + String charset = "US-ASCII"; + + switch (platformID) { + case PLATFORMID_UNICODE: + charset = "UTF-16"; + break; + case PLATFORMID_MICROSOFT: + charset = "UTF-16"; + break; + } + + return charset; + } + + /** Get a pretty string */ + @Override + public String toString() { + StringBuffer buf = new StringBuffer(); + String indent = " "; + + buf.append(indent + "Format: " + getFormat() + "\n"); + buf.append(indent + "Count : " + getCount() + "\n"); + + for (Iterator i = this.records.keySet().iterator(); i.hasNext();) { + NameRecord rec = (NameRecord) i.next(); + + buf.append(indent + " platformID: " + rec.platformID); + buf.append(" platformSpecificID: " + rec.platformSpecificID); + buf.append(" languageID: " + rec.languageID); + buf.append(" nameID: " + rec.nameID + "\n"); + buf.append(indent + " " + this.records.get(rec) + "\n"); + } + + return buf.toString(); + } + + public Collection getNames() + { + return Collections.unmodifiableCollection(records.values()); + } + + /** + * A class to hold the data associated with each record + */ + static class NameRecord implements Comparable { + /** + * Platform ID + */ + short platformID; + + /** + * Platform Specific ID (Encoding) + */ + short platformSpecificID; + + /** + * Language ID + */ + short languageID; + + /** + * Name ID + */ + short nameID; + + /** + * Create a new record + */ + NameRecord(short platformID, short platformSpecificID, + short languageID, short nameID) { + this.platformID = platformID; + this.platformSpecificID = platformSpecificID; + this.languageID = languageID; + this.nameID = nameID; + } + + + /** + * Compare two records + */ + @Override + public boolean equals(Object o) { + return (compareTo(o) == 0); + } + + /** + * Compare two records + */ + @Override + public int compareTo(Object obj) { + if (!(obj instanceof NameRecord)) { + return -1; + } + + NameRecord rec = (NameRecord) obj; + + if (this.platformID > rec.platformID) { + return 1; + } else if (this.platformID < rec.platformID) { + return -1; + } else if (this.platformSpecificID > rec.platformSpecificID) { + return 1; + } else if (this.platformSpecificID < rec.platformSpecificID) { + return -1; + } else if (this.languageID > rec.languageID) { + return 1; + } else if (this.languageID < rec.languageID) { + return -1; + } else if (this.nameID > rec.nameID) { + return 1; + } else if (this.nameID < rec.nameID) { + return -1; + } else { + return 0; + } + } + + + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/PostTable.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/PostTable.java new file mode 100644 index 0000000000..598ae68a12 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/PostTable.java @@ -0,0 +1,545 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.font.ttf; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +import com.github.librepdf.pdfrenderer.PDFDebugger; + +/** + * Model the TrueType Post table + * + * @author jkaplan + */ +public class PostTable extends TrueTypeTable { + + /** Holds value of property format. */ + private int format; + + /** Holds value of property italicAngle. */ + private int italicAngle; + + /** Holds value of property underlinePosition. */ + private short underlinePosition; + + /** Holds value of property underlineThickness. */ + private short underlineThickness; + + /** Holds value of property isFixedPitch. */ + private short isFixedPitch; + + /** Holds value of property minMemType42. */ + private int minMemType42; + + /** Holds value of property maxMemType42. */ + private int maxMemType42; + + /** Holds value of property minMemType1. */ + private int minMemType1; + + /** Holds value of property maxMemType1. */ + private int maxMemType1; + + /** A map which character values to names and vice versa */ + private PostMap nameMap; + + /** Creates a new instance of PostTable */ + protected PostTable() { + super (TrueTypeTable.POST_TABLE); + + this.nameMap = new PostMap(); + } + + /** + * Map a character name to a glyphNameIndex + */ + public short getGlyphNameIndex(String name) { + return this.nameMap.getCharIndex(name); + } + + /** + * Map a character code to a glyphIndex name + */ + public String getGlyphName(char c) { + return this.nameMap.getCharName(c); + } + + /** get the data in this map as a ByteBuffer */ + @Override + public ByteBuffer getData() { + int size = getLength(); + + ByteBuffer buf = ByteBuffer.allocate(size); + + // write the header + buf.putInt(getFormat()); + buf.putInt(getItalicAngle()); + buf.putShort(getUnderlinePosition()); + buf.putShort(getUnderlineThickness()); + buf.putShort(getIsFixedPitch()); + buf.putShort((short) 0); + buf.putInt(getMinMemType42()); + buf.putInt(getMaxMemType42()); + buf.putInt(getMinMemType1()); + buf.putInt(getMaxMemType1()); + + // now write the table + buf.put(this.nameMap.getData()); + + // reset the start pointer + buf.flip(); + + return buf; + } + + /** Initialize this structure from a ByteBuffer */ + @Override + public void setData(ByteBuffer data) { + setFormat(data.getInt()); + setItalicAngle(data.getInt()); + setUnderlinePosition(data.getShort()); + setUnderlineThickness(data.getShort()); + setIsFixedPitch(data.getShort()); + data.getShort(); + setMinMemType42(data.getInt()); + setMaxMemType42(data.getInt()); + setMinMemType1(data.getInt()); + setMaxMemType1(data.getInt()); + + // create the map, based on the type + switch (this.format) { + case 0x10000: + this.nameMap = new PostMapFormat0(); + break; + case 0x20000: + this.nameMap = new PostMapFormat2(); + break; + case 0x30000: + // empty post map. + this.nameMap = new PostMap(); + break; + default: + this.nameMap = new PostMap(); + PDFDebugger.debug("Unknown post map type: " + + Integer.toHexString(this.format)); + break; + } + + // fill in the data in the map + this.nameMap.setData(data); + } + + /** + * Get the length of this table + */ + @Override + public int getLength() { + int size = 32; + if (this.nameMap != null) { + size += this.nameMap.getLength(); + } + + return size; + } + + /** Getter for property format. + * @return Value of property format. + * + */ + public int getFormat() { + return this.format; + } + + /** Setter for property format. + * @param format New value of property format. + * + */ + public void setFormat(int format) { + this.format = format; + } + + /** Getter for property italicAngle. + * @return Value of property italicAngle. + * + */ + public int getItalicAngle() { + return this.italicAngle; + } + + /** Setter for property italicAngle. + * @param italicAngle New value of property italicAngle. + * + */ + public void setItalicAngle(int italicAngle) { + this.italicAngle = italicAngle; + } + + /** Getter for property underlinePosition. + * @return Value of property underlinePosition. + * + */ + public short getUnderlinePosition() { + return this.underlinePosition; + } + + /** Setter for property underlinePosition. + * @param underlinePosition New value of property underlinePosition. + * + */ + public void setUnderlinePosition(short underlinePosition) { + this.underlinePosition = underlinePosition; + } + + /** Getter for property underlineThickness. + * @return Value of property underlineThickness. + * + */ + public short getUnderlineThickness() { + return this.underlineThickness; + } + + /** Setter for property underlineThickness. + * @param underlineThickness New value of property underlineThickness. + * + */ + public void setUnderlineThickness(short underlineThickness) { + this.underlineThickness = underlineThickness; + } + + /** Getter for property isFixedPitch. + * @return Value of property isFixedPitch. + * + */ + public short getIsFixedPitch() { + return this.isFixedPitch; + } + + /** Setter for property isFixedPitch. + * @param isFixedPitch New value of property isFixedPitch. + * + */ + public void setIsFixedPitch(short isFixedPitch) { + this.isFixedPitch = isFixedPitch; + } + + /** Getter for property minMemType42. + * @return Value of property minMemType42. + * + */ + public int getMinMemType42() { + return this.minMemType42; + } + + /** Setter for property minMemType42. + * @param minMemType42 New value of property minMemType42. + * + */ + public void setMinMemType42(int minMemType42) { + this.minMemType42 = minMemType42; + } + + /** Getter for property maxMemType42. + * @return Value of property maxMemType42. + * + */ + public int getMaxMemType42() { + return this.maxMemType42; + } + + /** Setter for property maxMemType42. + * @param maxMemType42 New value of property maxMemType42. + * + */ + public void setMaxMemType42(int maxMemType42) { + this.maxMemType42 = maxMemType42; + } + + /** Getter for property minMemType1. + * @return Value of property minMemType1. + * + */ + public int getMinMemType1() { + return this.minMemType1; + } + + /** Setter for property minMemType1. + * @param minMemType1 New value of property minMemType1. + * + */ + public void setMinMemType1(int minMemType1) { + this.minMemType1 = minMemType1; + } + + /** Getter for property maxMemType1. + * @return Value of property maxMemType1. + * + */ + public int getMaxMemType1() { + return this.maxMemType1; + } + + /** Setter for property maxMemType1. + * @param maxMemType1 New value of property maxMemType1. + * + */ + public void setMaxMemType1(int maxMemType1) { + this.maxMemType1 = maxMemType1; + } + + /** An empty post map */ + class PostMap { + /** map a name to a character index */ + short getCharIndex(String charName) { + return (short) 0; + } + + /** name a character index to a name */ + String getCharName(char charIndex) { + return null; + } + + /** get the length of the data in this map */ + int getLength() { + return 0; + } + + /** get the data in this map as a ByteBuffer */ + ByteBuffer getData() { + return ByteBuffer.allocate(0); + } + + /** set the data in this map from a ByteBuffer */ + void setData(ByteBuffer data) { + // do nothing + return; + } + } + + /** A Format 0 post map */ + class PostMapFormat0 extends PostMap { + /** the glyph names in standard Macintosh ordering */ + protected final String stdNames[] = { +/* 0 */ ".notdef", ".null", "nonmarkingreturn", "space", "exclam", "quotedbl", "numbersign", "dollar", +/* 8 */ "percent", "ampersand", "quotesingle", "parenleft", "parenright", "asterisk", "plus", "comma", +/* 16 */ "hyphen", "period", "slash", "zero", "one", "two", "three", "four", +/* 24 */ "five", "six", "seven", "eight", "nine", "colon", "semicolon", "less", +/* 32 */ "equal", "greater", "question", "at", "A", "B", "C", "D", +/* 40 */ "E", "F", "G", "H", "I", "J", "K", "L", +/* 48 */ "M", "N", "O", "P", "Q", "R", "S", "T", +/* 56 */ "U", "V", "W", "X", "Y", "Z", "bracketleft", "ackslash", +/* 64 */ "bracketright", "asciicircum", "underscore", "grave", "a", "b", "c", "d", +/* 72 */ "e", "f", "g", "h", "i", "j", "k", "l", +/* 80 */ "m", "n", "o", "p", "q", "r", "s", "t", +/* 88 */ "u", "v", "w", "x", "y", "z", "braceleft", "bar", +/* 96 */ "braceright", "asciitilde", "Adieresis", "Aring", "Ccedilla", "Eacute", "Ntilde", "Odieresis", +/* 104 */ "Udieresis", "aacute", "agrave", "acircumflex", "adieresis", "atilde", "aring", "ccedilla", +/* 112 */ "eacute", "egrave", "ecircumflex", "edieresis", "iacute", "igrave", "icircumflex", "idieresis", +/* 120 */ "ntilde", "oacute", "ograve", "ocircumflex", "odieresis", "otilde", "uacute", "ugrave", +/* 128 */ "ucircumflex", "udieresis", "dagger", "degree", "cent", "sterling", "section", "bullet", +/* 136 */ "paragraph", "germandbls", "registered", "copyright", "trademark", "acute", "dieresis", "notequal", +/* 144 */ "AE", "Oslash", "infinity", "plusminus", "lessequal", "greaterequal", "yen", "mu", +/* 152 */ "partialdiff", "summation", "product", "pi", "integral", "ordfeminine", "ordmasculine", "Omega", +/* 160 */ "ae", "oslash", "questiondown", "exclamdown", "logicalnot", "radical", "florin", "approxequal", +/* 168 */ "Delta", "guillemotleft", "guillemotright", "ellipsis", "nonbreakingspace", "Agrave", "Atilde", "Otilde", +/* 176 */ "OE", "oe", "endash", "emdash", "quotedblleft", "quotedblright", "quoteleft", "quoteright", +/* 184 */ "divide", "lozenge", "ydieresis", "Ydieresis", "fraction", "currency", "guilsinglleft", "guilsinglright", +/* 192 */ "fi", "fl", "daggerdbl", "periodcentered", "quotesinglbase", "quotedblbase", "perthousand", "Acircumflex", +/* 200 */ "Ecircumflex", "Aacute", "Edieresis", "Egrave", "Iacute", "Icircumflex", "Idieresis", "Igrave", +/* 208 */ "Oacute", "Ocircumflex", "apple", "Ograve", "Uacute", "Ucircumflex", "Ugrave", "dotlessi", +/* 216 */ "circumflex", "tilde", "macron", "breve", "dotaccent", "ring", "cedilla", "hungarumlaut", +/* 224 */ "ogonek", "caron", "Lslash", "lslash", "Scaron", "scaron", "Zcaron", "zcaron", +/* 232 */ "brokenbar", "Eth", "eth", "Yacute", "yacute", "Thorn", "thorn", "minus", +/* 240 */ "multiply", "onesuperior", "twosuperior", "threesuperior", "onehalf", "onequarter", "threequarters", "franc", +/* 248 */ "Gbreve", "gbreve", "Idotaccent", "Scedilla", "scedilla", "Cacute", "cacute", "Ccaron", +/* 256 */ "ccaron", "dcroat" + }; + + @Override + /** map a name to a character index */ + short getCharIndex(String charName) { + for (int i = 0; i < this.stdNames.length; i++) { + if (charName.equals(this.stdNames[i])) { + return (short) i; + } + } + + return (short) 0; + } + + @Override + /** name a character index to a name */ + String getCharName(char charIndex) { + return this.stdNames[charIndex]; + } + + @Override + /** get the length of the data in this map */ + int getLength() { + return 0; + } + + @Override + /** get the data in this map as a ByteBuffer */ + ByteBuffer getData() { + return ByteBuffer.allocate(0); + } + + @Override + /** set the data in this map from a ByteBuffer */ + void setData(ByteBuffer data) { + // do nothing + return; + } + } + + /** an extension to handle format 2 post maps */ + class PostMapFormat2 extends PostMapFormat0 { + /** the glyph name index */ + short[] glyphNameIndex; + + /** the glyph names */ + String[] glyphNames; + + @Override + /** Map a character name to an index */ + short getCharIndex(String charName) { + // find the index of this character name + short idx = -1; + + // first try the local names map + for (int i = 0; i < this.glyphNames.length; i++) { + if (charName.equals(this.glyphNames[i])) { + // this is the value from the glyph name index + idx = (short) (this.stdNames.length + i); + break; + } + } + + // if that doesn't work, try the standard names + if (idx == -1) { + idx = super.getCharIndex(charName); + } + + // now get the entry in the index + for (int c = 0; c < this.glyphNameIndex.length; c++) { + if (this.glyphNameIndex[c] == idx) { + return (short) c; + } + } + + // not found + return (short) 0; + } + + @Override + /** Map an index to a character name */ + String getCharName(char charIndex) { + if (charIndex >= this.stdNames.length) { + return this.glyphNames[charIndex - this.stdNames.length]; + } + + return super.getCharName(charIndex); + } + + @Override + /** get the length of this class's data */ + int getLength() { + // the size of the header plus the table of mappings + int size = 2 + (2 * this.glyphNameIndex.length); + + // the size of each string -- note the extra byte for a pascal + // string + for (int i = 0; i < this.glyphNames.length; i++) { + size += this.glyphNames[i].length() + 1; + } + + return size; + } + + @Override + /** get the data in this map as a byte array */ + ByteBuffer getData() { + ByteBuffer buf = ByteBuffer.allocate(getLength()); + + // write the number of glyphs + buf.putShort((short) this.glyphNameIndex.length); + + // write the name indices + for (int i = 0; i < this.glyphNameIndex.length; i++) { + buf.putShort(this.glyphNameIndex[i]); + } + + // write the names as pascal strings + for (int i = 0; i < this.glyphNames.length; i++) { + buf.put((byte) this.glyphNames[i].length()); + buf.put(this.glyphNames[i].getBytes()); + } + + // reset the start pointer + buf.flip(); + + return buf; + } + + @Override + /** set the contents of this map from a ByteBuffer */ + void setData(ByteBuffer data) { + short numGlyphs = data.getShort(); + this.glyphNameIndex = new short[numGlyphs]; + + // the highest glyph index seen so far + int maxGlyph = 257; + for (int i = 0; i < numGlyphs; i++) { + this.glyphNameIndex[i] = data.getShort(); + + // see if this is the highest glyph + if (this.glyphNameIndex[i] > maxGlyph) { + maxGlyph = this.glyphNameIndex[i]; + } + } + + // subtract off the default glyphs + maxGlyph -= 257; + + // read in any additional names + this.glyphNames = new String[maxGlyph]; + // fill with empty strings for avoiding nullpointer exception: glyph names + // are not mandatory for true type fonts according to the PDF spec. + Arrays.fill(this.glyphNames, ""); + + // read each name from a pascal string + // the length is stored in the first byte, followed by + // the data + for (int i = 0; i < maxGlyph; i++) { + if(data.hasRemaining()) { + // size in the first byte + byte size = data.get(); + + // then the data + byte[] stringData = new byte[size]; + data.get(stringData); + + this.glyphNames[i] = new String(stringData); + } + } + } + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/TrueTypeFont.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/TrueTypeFont.java new file mode 100644 index 0000000000..eebcf870d0 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/TrueTypeFont.java @@ -0,0 +1,468 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.font.ttf; + +import java.awt.Font; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +import com.github.librepdf.pdfrenderer.BaseWatchable; +import com.github.librepdf.pdfrenderer.PDFDebugger; + +/** + * + * @author jkaplan + */ +public class TrueTypeFont { + + private final int type; + // could be a ByteBuffer or a TrueTypeTable + + private final SortedMap tables; + + /** Creates a new instance of TrueTypeParser */ + public TrueTypeFont (int type) { + this.type = type; + + this.tables = Collections.synchronizedSortedMap ( + new TreeMap ()); + } + + /** + * Parses a TrueType font from a byte array + */ + public static TrueTypeFont parseFont (byte[] orig) { + ByteBuffer inBuf = ByteBuffer.wrap (orig); + return parseFont (inBuf); + } + + /** + * Parses a TrueType font from a byte buffer + */ + public static TrueTypeFont parseFont (ByteBuffer inBuf) { + int type = inBuf.getInt (); + short numTables = inBuf.getShort (); + @SuppressWarnings("unused") + short searchRange = inBuf.getShort (); + @SuppressWarnings("unused") + short entrySelector = inBuf.getShort (); + @SuppressWarnings("unused") + short rangeShift = inBuf.getShort (); + + TrueTypeFont font = new TrueTypeFont (type); + parseDirectories (inBuf, numTables, font); + + return font; + } + + /** + * Get the type of this font + */ + public int getType () { + return this.type; + } + + /** + * Add a table to the font + * + * @param tagString the name of this table, as a 4 character string + * (i.e. cmap or head) + * @param data the data for this table, as a byte buffer + */ + public void addTable (String tagString, ByteBuffer data) { + this.tables.put (tagString, data); + } + + /** + * Add a table to the font + * + * @param tagString the name of this table, as a 4 character string + * (i.e. cmap or head) + * @param table the table + */ + public void addTable (String tagString, TrueTypeTable table) { + this.tables.put (tagString, table); + } + + /** + * Get a table by name. This command causes the table in question + * to be parsed, if it has not already been parsed. + * + * @param tagString the name of this table, as a 4 character string + * (i.e. cmap or head) + */ + public TrueTypeTable getTable (String tagString) { + Object tableObj = this.tables.get (tagString); + + TrueTypeTable table = null; + + if (tableObj instanceof ByteBuffer) { + // the table has not yet been parsed. Parse it, and add the + // parsed version to the map of tables. + ByteBuffer data = (ByteBuffer) tableObj; + + table = TrueTypeTable.createTable (this, tagString, data); + addTable (tagString, table); + } else { + table = (TrueTypeTable) tableObj; + } + + return table; + } + + /** + * Remove a table by name + * + * @param tagString the name of this table, as a 4 character string + * (i.e. cmap or head) + */ + public void removeTable (String tagString) { + this.tables.remove (tagString); + } + + /** + * Get the number of tables + */ + public short getNumTables () { + return (short) this.tables.size (); + } + + /** + * Get the search range + */ + public short getSearchRange () { + double pow2 = Math.floor (Math.log (getNumTables ()) / Math.log (2)); + double maxPower = Math.pow (2, pow2); + + return (short) (16 * maxPower); + } + + /** + * Get the entry selector + */ + public short getEntrySelector () { + double pow2 = Math.floor (Math.log (getNumTables ()) / Math.log (2)); + double maxPower = Math.pow (2, pow2); + + return (short) (Math.log (maxPower) / Math.log (2)); + } + + /** + * Get the range shift + */ + public short getRangeShift () { + double pow2 = Math.floor (Math.log (getNumTables ()) / Math.log (2)); + double maxPower = Math.pow (2, pow2); + + return (short) ((maxPower * 16) - getSearchRange ()); + } + + /** + * Write a font given the type and an array of Table Directory Entries + */ + public byte[] writeFont () { + // allocate a buffer to hold the font + ByteBuffer buf = ByteBuffer.allocate (getLength ()); + + // write the font header + buf.putInt (getType ()); + buf.putShort (getNumTables ()); + buf.putShort (getSearchRange ()); + buf.putShort (getEntrySelector ()); + buf.putShort (getRangeShift ()); + + // first offset is the end of the table directory entries + int curOffset = 12 + (getNumTables () * 16); + + // write the tables + for (Iterator i = this.tables.keySet ().iterator (); i.hasNext ();) { + String tagString = i.next (); + int tag = TrueTypeTable.stringToTag (tagString); + + ByteBuffer data = null; + + Object tableObj = this.tables.get (tagString); + if (tableObj instanceof TrueTypeTable) { + data = ((TrueTypeTable) tableObj).getData (); + } else { + data = (ByteBuffer) tableObj; + } + + int dataLen = data.remaining (); + + // write the table directory entry + buf.putInt (tag); + buf.putInt (calculateChecksum (tagString, data)); + buf.putInt (curOffset); + buf.putInt (dataLen); + + // save the current position + buf.mark (); + + // move to the current offset and write the data + buf.position (curOffset); + buf.put (data); + + // reset the data start pointer + data.flip (); + + // return to the table directory entry + buf.reset (); + + // udate the offset + curOffset += dataLen; + + // don't forget the padding + while ((curOffset % 4) > 0) { + curOffset++; + } + } + + buf.position (curOffset); + buf.flip (); + + // adjust the checksum + updateChecksumAdj (buf); + + return buf.array (); + } + + /** + * Calculate the checksum for a given table + * + * @param tagString the name of the data + * @param data the data in the table + */ + private static int calculateChecksum (String tagString, ByteBuffer data) { + int sum = 0; + + data.mark (); + + // special adjustment for head table: always treat the 4-bytes + // starting at byte 8 as 0x0000. This the checkSumAdjustment so + // must be ignored here (see the TTF spec) + if (tagString.equals ("head")) { + if(!data.isReadOnly()) { + data.putInt (8, 0); + } + sum += data.getInt(); + sum += data.getInt(); + // consume the uncounted checkSumAdjustment int + data.getInt(); + } + + int nlongs = (data.remaining () + 3) / 4; + + while (nlongs-- > 0) { + if (data.remaining () > 3) { + sum += data.getInt (); + } else { + byte b0 = (data.remaining () > 0) ? data.get () : 0; + byte b1 = (data.remaining () > 0) ? data.get () : 0; + byte b2 = (data.remaining () > 0) ? data.get () : 0; + + sum += ((0xff & b0) << 24) | ((0xff & b1) << 16) | + ((0xff & b2) << 8); + } + } + + data.reset (); + + return sum; + } + + /** + * Get directory entries from a font + */ + private static void parseDirectories (ByteBuffer data, int numTables, + TrueTypeFont ttf) { + for (int i = 0; i < numTables; i++) { + int tag = data.getInt (); + String tagString = TrueTypeTable.tagToString (tag); + PDFDebugger.debug("TTFFont.parseDirectories: " + tagString, 100); + int checksum = data.getInt (); + int offset = data.getInt (); + int length = data.getInt (); + + // read the data + PDFDebugger.debug("TTFFont.parseDirectories: checksum: " + + checksum + ", offset: " + offset + ", length: " + length, 100); + data.mark (); + data.position (offset); + + ByteBuffer tableData = data.slice (); + tableData.limit (length); + + int calcChecksum = calculateChecksum (tagString, tableData); + + if (calcChecksum == checksum) { + ttf.addTable (tagString, tableData); + } else { + PDFDebugger.debug("Mismatched checksums on table " + tagString + ": " + calcChecksum + " != " + checksum, 200); + + ttf.addTable (tagString, tableData); + + } + data.reset (); + } + } + + /** + * Get the length of the font + * + * @return the length of the entire font, in bytes + */ + private int getLength () { + // the size of all the table directory entries + int length = 12 + (getNumTables () * 16); + + // for each directory entry, get the size, + // and don't forget the padding! + for (Iterator i = this.tables.values ().iterator (); i.hasNext ();) { + Object tableObj = i.next (); + + // add the length of the entry + if (tableObj instanceof TrueTypeTable) { + length += ((TrueTypeTable) tableObj).getLength (); + } else { + length += ((ByteBuffer) tableObj).remaining (); + } + + // pad + if ((length % 4) != 0) { + length += (4 - (length % 4)); + } + } + + return length; + } + + /** + * Update the checksumAdj field in the head table + */ + private void updateChecksumAdj (ByteBuffer fontData) { + int checksum = calculateChecksum ("", fontData); + int checksumAdj = 0xb1b0afba - checksum; + + // find the head table + int offset = 12 + (getNumTables () * 16); + + // find the head table + for (Iterator i = this.tables.keySet ().iterator (); i.hasNext ();) { + String tagString = i.next (); + + // adjust the checksum + if (tagString.equals ("head")) { + fontData.putInt (offset + 8, checksumAdj); + return; + } + + // add the length of the entry + Object tableObj = this.tables.get (tagString); + if (tableObj instanceof TrueTypeTable) { + offset += ((TrueTypeTable) tableObj).getLength (); + } else { + offset += ((ByteBuffer) tableObj).remaining (); + } + + // pad + if ((offset % 4) != 0) { + offset += (4 - (offset % 4)); + } + } + } + + /** + * Write the font to a pretty string + */ + @Override + public String toString () { + StringBuffer buf = new StringBuffer (); + + System.out.println ("Type : " + getType ()); + System.out.println ("NumTables : " + getNumTables ()); + System.out.println ("SearchRange : " + getSearchRange ()); + System.out.println ("EntrySelector: " + getEntrySelector ()); + System.out.println ("RangeShift : " + getRangeShift ()); + + for (Iterator> i = this.tables.entrySet ().iterator (); i.hasNext ();) { + Map.Entry e = i.next (); + + TrueTypeTable table = null; + if (e.getValue () instanceof ByteBuffer) { + table = getTable (e.getKey ()); + } else { + table = (TrueTypeTable) e.getValue (); + } + + System.out.println (table); + } + + return buf.toString (); + } + + public Collection getNames() { + NameTable table = (NameTable) getTable("name"); + if (table != null) { + return table.getNames(); + } else { + return Collections.emptyList(); + } + } + + /** + * @param args the command line arguments + */ + public static void main (String[] args) { + if (args.length != 1) { + System.out.println ("Usage: "); + System.out.println (" TrueTypeParser "); + System.exit (-1); + } + + try { + RandomAccessFile raf = new RandomAccessFile (args[0], "r"); + + int size = (int) raf.length (); + byte[] data = new byte[size]; + + raf.readFully (data); + + TrueTypeFont ttp = TrueTypeFont.parseFont (data); + + System.out.println (ttp); + + InputStream fontStream = new ByteArrayInputStream (ttp.writeFont ()); + + @SuppressWarnings("unused") + Font f = Font.createFont (Font.TRUETYPE_FONT, fontStream); + raf.close(); + } catch (Exception e) { + BaseWatchable.getErrorHandler().publishException(e); + } + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/TrueTypeTable.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/TrueTypeTable.java new file mode 100644 index 0000000000..c882238407 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/TrueTypeTable.java @@ -0,0 +1,194 @@ +/* Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.font.ttf; + +import java.nio.ByteBuffer; + +/** + * The base class for TrueType tables. Specific tables can extend this + * to add more functionality + */ +public class TrueTypeTable { + + /** + * Well known tables + */ + public static final int CMAP_TABLE = 0x636d6170; + public static final int GLYF_TABLE = 0x676c7966; + public static final int HEAD_TABLE = 0x68656164; + public static final int HHEA_TABLE = 0x68686561; + public static final int HMTX_TABLE = 0x686d7478; + public static final int MAXP_TABLE = 0x6d617870; + public static final int NAME_TABLE = 0x6e616d65; + public static final int POST_TABLE = 0x706f7374; + public static final int LOCA_TABLE = 0x6c6f6361; + /** + * This table's tag + */ + private int tag; + /** + * The data in this table, in ByteBuffer form + */ + private ByteBuffer data; + + /** + * Creates a new instance of TrueTypeTable. + * + * This method is protected. Use the getTable() methods + * to get new instances. + * + * @param tag the tag for this table + */ + protected TrueTypeTable(int tag) { + this.tag = tag; + } + + /** + * Get a new instance of an empty table by tag string + * + * @param ttf the font that contains this table + * @param tagString the tag for this table, as a 4 character string + * (e.g. head or cmap) + */ + public static TrueTypeTable createTable(TrueTypeFont ttf, + String tagString) { + return createTable(ttf, tagString, null); + } + + /** + * Get a new instance of a table with provided data + * + * @param ttf the font that contains this table + * @param tagString the tag for this table, as a 4 character string + * (e.g. head or cmap) + * @param data the table data + */ + public static TrueTypeTable createTable(TrueTypeFont ttf, + String tagString, ByteBuffer data) { + TrueTypeTable outTable = null; + + int tag = stringToTag(tagString); + + switch (tag) { + case CMAP_TABLE: // cmap table + outTable = new CmapTable(); + break; + case GLYF_TABLE: + outTable = new GlyfTable(ttf); + break; + case HEAD_TABLE: // head table + outTable = new HeadTable(); + break; + case HHEA_TABLE: // hhea table + outTable = new HheaTable(); + break; + case HMTX_TABLE: + outTable = new HmtxTable(ttf); + break; + case LOCA_TABLE: + outTable = new LocaTable(ttf); + break; + case MAXP_TABLE: // maxp table + outTable = new MaxpTable(); + break; + case NAME_TABLE: // name table + outTable = new NameTable(); + break; + case POST_TABLE: // post table + outTable = new PostTable(); + break; + default: + outTable = new TrueTypeTable(tag); + break; + } + + if (data != null) { + outTable.setData(data); + } + + return outTable; + } + + /** + * Get the table's tag + */ + public int getTag() { + return this.tag; + } + + /** + * Get the data in the table + */ + public ByteBuffer getData() { + return this.data; + } + + /** + * Set the data in the table + */ + public void setData(ByteBuffer data) { + this.data = data; + } + + /** + * Get the size of the table, in bytes + */ + public int getLength() { + return getData().remaining(); + } + + /** + * Get the tag as a string + */ + public static String tagToString(int tag) { + char[] c = new char[4]; + c[0] = (char) (0xff & (tag >> 24)); + c[1] = (char) (0xff & (tag >> 16)); + c[2] = (char) (0xff & (tag >> 8)); + c[3] = (char) (0xff & (tag)); + + return new String(c); + } + + /** + * Turn a string into a tag + */ + public static int stringToTag(String tag) { + char[] c = tag.toCharArray(); + + if (c.length != 4) { + throw new IllegalArgumentException("Bad tag length: " + tag); + } + + return c[0] << 24 | c[1] << 16 | c[2] << 8 | c[3]; + } + + /** + * Put into a nice string + */ + @Override + public String toString() { + String out = " " + tagToString(getTag()) + " Table. Data is: "; + if (getData() == null) { + out += "not set"; + } else { + out += "set"; + } + return out; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/resource/glyphlist.txt b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/resource/glyphlist.txt new file mode 100644 index 0000000000..b9d451a873 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/font/ttf/resource/glyphlist.txt @@ -0,0 +1,4323 @@ +# ################################################################################### +# 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/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/FunctionType0.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/FunctionType0.java new file mode 100644 index 0000000000..b56738b7cf --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/FunctionType0.java @@ -0,0 +1,444 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.function; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import com.github.librepdf.pdfrenderer.PDFDebugger; +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/** + * A sampled function maps input values to output values by interpolating + * along a line or cubic between two known values. + */ +public class FunctionType0 extends PDFFunction { + /** the valid interpolation methods */ + protected static final int LINEAR_INTERPOLATION = 1; + protected static final int CUBIC_INTERPOLATION = 3; + + /** the size of each input dimension, as an array of m integers */ + private int[] size; + + /** the number of bits in each sample */ + private int bitsPerSample; + + /** the interpolation type, from the list above */ + private int order = 1; + + /** the optional encoding array, tells how to map input parameters to values */ + private float[] encode; + + /** the optional decoding array, tells how to map output parameters to values */ + private float[] decode; + + /** + * the actual samples, converted to integers. The first index is + * input values (from 0 to size[m - 1] * size[m - 2] * ... * size[0]), + * and the second is the output dimension within the sample (from 0 to n) + */ + private int[][] samples; + + /** Creates a new instance of FunctionType0 */ + protected FunctionType0() { + super (TYPE_0); + } + + /** Read the function information from a PDF Object */ + @Override + protected void parse(PDFObject obj) throws IOException { + // read the size array (required) + PDFObject sizeObj = obj.getDictRef("Size"); + if (sizeObj == null) { + throw new PDFParseException("Size required for function type 0!"); + } + PDFObject[] sizeAry = sizeObj.getArray(); + int[] size = new int[sizeAry.length]; + for (int i = 0; i < sizeAry.length; i++) { + size[i] = sizeAry[i].getIntValue(); + } + setSize(size); + + // read the # bits per sample (required) + PDFObject bpsObj = obj.getDictRef("BitsPerSample"); + if (bpsObj == null) { + throw new PDFParseException("BitsPerSample required for function type 0!"); + } + setBitsPerSample(bpsObj.getIntValue()); + + // read the order (optional) + PDFObject orderObj = obj.getDictRef("Order"); + if (orderObj != null) { + setOrder(orderObj.getIntValue()); + } + + // read the encode array (optional) + PDFObject encodeObj = obj.getDictRef("Encode"); + if (encodeObj != null) { + PDFObject[] encodeAry = encodeObj.getArray(); + float[] encode = new float[encodeAry.length]; + for (int i = 0; i < encodeAry.length; i++) { + encode[i] = encodeAry[i].getFloatValue(); + } + setEncode(encode); + } + + // read the decode array (optional) + PDFObject decodeObj = obj.getDictRef("Decode"); + if (decodeObj != null) { + PDFObject[] decodeAry = decodeObj.getArray(); + float[] decode = new float[decodeAry.length]; + for (int i = 0; i < decodeAry.length; i++) { + decode[i] = decodeAry[i].getFloatValue(); + } + setDecode(decode); + } + + // finally, read the samples + setSamples(readSamples(obj.getStreamBuffer())); + } + + /** + * Map from m input values to n output values. + * The number of inputs m must be exactly one half the size of the + * domain. The number of outputs should match one half the size of the + * range. + * + * @param inputs an array of m input values + * @param outputs an array of size n which will be filled + * with the output values, or null to return a new array + */ + @Override + protected void doFunction(float[] inputs, int inputOffset, + float[] outputs, int outputOffset) + { + // calculate the encoded values for each input + float[] encoded = new float[getNumInputs()]; + for (int i = 0; i < getNumInputs(); i++) { + // encode -- interpolate(x, domain<2i>, domain<2i + 1>, + // encode<2i>, encode<2i + 1>) + encoded[i] = interpolate(inputs[i + inputOffset], + getDomain(2 * i), + getDomain((2 * i) + 1), + getEncode(2 * i), + getEncode((2 * i) + 1)); + + // clip to size of sample table -- min(max(e, 0), size - 1) + encoded[i] = Math.max(encoded[i], 0); + encoded[i] = Math.min(encoded[i], this.size[i] - 1); + } + + // do some magic + for (int i = 0; i < getNumOutputs(); i++) { + if (getOrder() == 1) { + outputs[i + outputOffset] = multilinearInterpolate(encoded, i); + } else { + outputs[i + outputOffset] = multicubicInterpolate(encoded, i); + } + } + + // now adjust the output to be within range + for (int i = 0; i < outputs.length; i++) { + // decode -- interpolate(r, 0, 2^bps - 1, + // decode<2i>, decode<2i + 1>) + outputs[i + outputOffset] = interpolate(outputs[i + outputOffset], + 0, + (float) Math.pow(2, getBitsPerSample()) - 1, + getDecode(2 * i), + getDecode((2 * i) + 1)); + } + } + + /** + * Get the size of a given input dimension + * + * @param dimension the input dimension to get the size of + * @return the number of samples in the given dimension + */ + protected int getSize(int dimension) { + return this.size[dimension]; + } + + /** + * Set the size of all input dimensions + */ + protected void setSize(int[] size) { + this.size = size; + } + + + /** + * Get the number of bits per sample + */ + protected int getBitsPerSample() { + return this.bitsPerSample; + } + + /** + * Set the number of bits per sample + */ + protected void setBitsPerSample(int bits) { + this.bitsPerSample = bits; + } + + /** + * Get the interpolation type + */ + protected int getOrder() { + return this.order; + } + + /** + * Set the interpolation type + */ + protected void setOrder(int order) { + this.order = order; + } + + /** + * Get the encoding for a particular input parameter + * + * @param i the index into the encoding array, which has size 2 * m. + * the ith entry in the array has index 2i, + * 2i + 1 + * @return the encoding value if the encoding array is set, or the default + */ + protected float getEncode(int i) { + if (this.encode != null) { + return this.encode[i]; + } else if ((i % 2) == 0) { + return 0f; + } else { + return (getSize(i / 2) - 1); + } + } + + /** + * Set the encode array + */ + protected void setEncode(float[] encode) { + this.encode = encode; + } + + /** + * Get the decoding for a particular input parameter + * + * @param i the index into the decoding array, which has size 2 * n. + * the ith entry in the array has index 2i, + * 2i + 1 + * @return the decoding value if the decoding array is set, or the default + */ + protected float getDecode(int i) { + if (this.decode != null) { + return this.decode[i]; + } else { + return getRange(i); + } + } + + /** + * Set the decode array + */ + protected void setDecode(float[] decode) { + this.decode = decode; + } + + /** + * Get a component for a sample given m indices and output + * dimension. + * + * @param values an array of m values determining which sample + * to select + * @param od the output dimension (0 - n) to get the sample in + * @return the sample for the given values and index + */ + protected int getSample(int[] values, int od) { + int mult = 1; + int index = 0; + for (int i = 0; i < values.length; i++) { + index += mult * values[i]; + mult *= getSize(i); + } + + return this.samples[index][od]; + } + + /** + * Set the table of samples + */ + protected void setSamples(int[][] samples) { + this.samples = samples; + } + + /** + * Read the samples from the input stream. Each sample is made up + * of n components, each of which has length bitsPerSample + * bits. The samples are arranged by dimension, then range + */ + private int[][] readSamples(ByteBuffer buf) { + // calculate the number of samples in the table + int size = 1; + for (int i = 0; i < getNumInputs(); i++) { + size *= getSize(i); + } + + // create the samples table + int[][] samples = new int[size][getNumOutputs()]; + + + // the current location in the buffer, in bits from byteLoc + int bitLoc = 0; + + // the current location in the buffer, in bytes + int byteLoc = 0; + + // the current index in the samples array + int index = 0; + + for (int i = 0; i < getNumInputs(); i++) { + for (int j = 0; j < getSize(i); j++) { + for (int k = 0; k < getNumOutputs(); k++) { + /** [JK FIXME one bit at a time is really inefficient */ + int value = 0; + + int toRead = getBitsPerSample(); + byte curByte = buf.get(byteLoc); + + while (toRead > 0) { + int nextBit = ((curByte >> (7 - bitLoc)) & 0x1); + value |= nextBit << (toRead - 1); + + if (++bitLoc == 8) { + bitLoc = 0; + byteLoc++; + + if (toRead > 1) { + curByte = buf.get(byteLoc); + } + } + + toRead--; + } + + samples[index][k] = value; + } + + index++; + } + } + + return samples; + } + + /** + * Perform a piecewise multilinear interpolation. The provides a + * close approximation to the standard linear interpolation, at + * a far lower cost, since every element is not evaluated at every + * iteration. Instead, a walk of the most significant axes is performed, + * following the algorithm desribed at: + * http://osl.iu.edu/~tveldhui/papers/MAScThesis/node33.html + * + * @param encoded the encoded input values + * @param od the output dimension + */ + private float multilinearInterpolate(float[] encoded, int od) { + // first calculate the distances -- the differences between + // each encoded value and the integer below it. + float[] dists = new float[encoded.length]; + + for (int i = 0; i < dists.length; i++) { + dists[i] = (float) (encoded[i] - Math.floor(encoded[i])); + } + + // initialize the map of axes. Each bit in this map represents + // whether the control value in that dimension should be the integer + // above or below encoded[i] + int map = 0; + + // the initial values + float val = getSample(encoded, map, od); + float prev = val; + + // walk the axes + for (int i = 0; i < dists.length; i++) { + // find the largest value of dist remaining + int idx = 0; + float largest = -1; + for (int c = 0; c < dists.length; c++) { + if (dists[c] > largest) { + largest = dists[c]; + idx = c; + } + } + + // now find the sample with that axis set to 1 + map |= (0x1 << idx); + float cur = getSample(encoded, map, od); + + // calculate the value and remember it + val += dists[idx] * (cur - prev); + prev = val; + + // make sure we won't find this distance again + dists[idx] = -1; + } + + // voila + return val; + } + + /** + * Perform a multicubic interpolation + * + * @param encoded the encoded input values + * @param od the output dimension + */ + private float multicubicInterpolate(float[] encoded, int od) { + PDFDebugger.debug("Cubic interpolation not supported!"); + return multilinearInterpolate(encoded, od); + } + + /** + * Get a sample based on an array of encoded values and a control + * map. For each bit in the map, if that bit is 0 the integer below + * the encoded value is selected, or if the bit is 1, the interger + * above is selected. + * + * @param encoded the encoded values + * @param map the bit map of control values + * @param od the output dimension to read the sample for + */ + private float getSample(float[] encoded, int map, int od) { + int[] controls = new int[encoded.length]; + + // fill in the controls array with appropriate ints + for (int i = 0; i < controls.length; i++) { + if ((map & (0x1 << i)) == 0) { + controls[i] = (int) Math.floor(encoded[i]); + } else { + controls[i] = (int) Math.ceil(encoded[i]); + } + } + + // now return the actual sample + return getSample(controls, od); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/FunctionType2.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/FunctionType2.java new file mode 100644 index 0000000000..686ceb2187 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/FunctionType2.java @@ -0,0 +1,150 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.function; + +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/** + * A type 2 function is an exponential interpolation function, which maps + * from one input value to n output values using a simple exponential + * formula. + */ +public class FunctionType2 extends PDFFunction { + /** the function's value at zero for the n outputs */ + private float[] c0 = new float[] { 0f }; + + /** the function's value at one for the n outputs */ + private float[] c1 = new float[] { 1f }; + + /** the exponent */ + private float n; + + /** Creates a new instance of FunctionType2 */ + public FunctionType2() { + super(TYPE_2); + } + + /** + * Read the zeros, ones and exponent + */ + @Override + protected void parse(PDFObject obj) throws IOException + { + // read the exponent (required) + PDFObject nObj = obj.getDictRef("N"); + if (nObj == null) { + throw new PDFParseException("Exponent required for function type 2!"); + } + setN(nObj.getFloatValue()); + + // read the zeros array (optional) + PDFObject cZeroObj = obj.getDictRef("C0"); + if (cZeroObj != null) { + PDFObject[] cZeroAry = cZeroObj.getArray(); + float[] cZero = new float[cZeroAry.length]; + for (int i = 0; i < cZeroAry.length; i++) { + cZero[i] = cZeroAry[i].getFloatValue(); + } + setC0(cZero); + } + + // read the ones array (optional) + PDFObject cOneObj = obj.getDictRef("C1"); + if (cOneObj != null) { + PDFObject[] cOneAry = cOneObj.getArray(); + float[] cOne = new float[cOneAry.length]; + for (int i = 0; i < cOneAry.length; i++) { + cOne[i] = cOneAry[i].getFloatValue(); + } + setC1(cOne); + } + } + + /** + * Calculate the function value for the input. For each output (j), + * the function value is: + * C0(j) + x^N * (C1(j) - C0(j)) + */ + @Override + protected void doFunction(float[] inputs, int inputOffset, + float[] outputs, int outputOffset) + { + // read the input value + float input = inputs[inputOffset]; + + // calculate the output values + for (int i = 0; i < getNumOutputs(); i++) { + outputs[i + outputOffset] = getC0(i) + + (float) (Math.pow(input, getN()) * (getC1(i) - getC0(i))); + } + } + + @Override + public int getNumOutputs() + { + // For Type 2 functions, the number of outputs is determined by the size of C0 (or C1). + return c0.length; + } + + /** + * Get the exponent + */ + public float getN() { + return this.n; + } + + /** + * Set the exponent + */ + protected void setN(float n) { + this.n = n; + } + + /** + * Get the values at zero + */ + public float getC0(int index) { + return this.c0[index]; + } + + /** + * Set the values at zero + */ + protected void setC0(float[] c0) { + this.c0 = c0; + } + + /** + * Get the values at one + */ + public float getC1(int index) { + return this.c1[index]; + } + + /** + * Set the values at one + */ + protected void setC1(float[] c1) { + this.c1 = c1; + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/FunctionType3.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/FunctionType3.java new file mode 100644 index 0000000000..3d8f824d91 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/FunctionType3.java @@ -0,0 +1,183 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.function; + +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/** + * 3.9.3 - A stitching function define a stitching of the subdomains of + * several 1-input functions to produce a single new 1-input function. + * Since the resulting stitching function is a 1-input function, the + * domain is given by a two-element array, [ Domain0 Domain1 ]. + * + *
    + * Example 4.25
    + * 5 0 obj                          % Shading dictionary
    + *  << /ShadingType 3
    + *      /ColorSpace /DeviceCMYK
    + *      /Coords [ 0.0 0.0 0.096 0.0 0.0 1.0 00]% Concentric circles
    + *      /Function 10 0 R
    + *      /Extend [ true true ]
    + *  >>
    + * endobj
    + *
    + * 10 0 obj                         % Color function
    + *  << /FunctionType 3
    + *      /Domain [ 0.0 1.0 ]
    + *      /Functions [ 11 0 R 12 0 R ]
    + *      /Bounds [ 0.708 ]
    + *      /Encode [ 1.0 0.0 0.0 1.0 ]
    + *  >>
    + * endobj
    + *
    + * 11 0 obj                         % First subfunction
    + *  << /FunctionType 2
    + *      /Domain [ 0.0 1.0 ]
    + *      /C0 [ 0.929 0.357 1.000 0.298 ]
    + *      /C1 [ 0.631 0.278 1.000 0.027 ]
    + *      /N 1.048
    + *  >>
    + * endobj
    + *
    + * 12 0 obj                         % Second subfunction
    + *  << /FunctionType 2
    + *      /Domain [ 0.0 1.0 ]
    + *      /C0 [ 0.929 0.357 1.000 0.298 ]
    + *      /C1 [ 0.941 0.400 1.000 0.102 ]
    + *      /N 1.374
    + *  >>
    + * endobj
    + * 
    + */ +public class FunctionType3 extends PDFFunction { + + private PDFFunction[] functions; + private float[] bounds; + private float[] encode; + + /** Creates a new instance of FunctionType3 */ + protected FunctionType3() { + super(TYPE_3); + } + + /** + *

    Read the function information from a PDF Object.

    + *

    Required entries ( Table 3.38) (3200-1:2008:7.10.4, table: 41) + * are:

  • + * + * Functions array (Required) An array of k 1-input functions making up + * the stitching function. The output dimensionality of all functions + * must be the same, and compatible with the value of Range + * if Range is present.
  • + * + * Domainarray (Required) A 2 element array where + * Domain0 is less than Domain1. This is read by the + * PDFFunction superclass.
  • + * + * Bounds array (Required) An array of k-1 numbers that, + * in combination with Domain, define the intervals to which each + * function from the Functions array applies. Bounds elements + * must be in order of increasing value, and each value must be within + * the domain defined by >b>Domain.
  • + * + * Encode array (Required) An array of 2 * k numbers that, + * taken in pairs, map each subset of the domain defined by + * and the Bounds array to the domain of the corresponding function. + *
  • + */ + @Override + protected void parse(PDFObject obj) throws IOException { + // read the Functions array (required) + PDFObject functionsObj = obj.getDictRef("Functions"); + if (functionsObj == null) { + throw new PDFParseException("Functions required for function type 3!"); + } + PDFObject[] functionsAry = functionsObj.getArray(); + functions = new PDFFunction[functionsAry.length]; + for (int i = 0; i < functionsAry.length; i++) { + functions[i] = getFunction(functionsAry[i]); + } + + // read the Bounds array (required) + PDFObject boundsObj = obj.getDictRef("Bounds"); + if (boundsObj == null) { + throw new PDFParseException("Bounds required for function type 3!"); + } + PDFObject[] boundsAry = boundsObj.getArray(); + bounds = new float[boundsAry.length + 2]; + if (bounds.length - 2 != functions.length - 1) { + throw new PDFParseException("Bounds array must be of length " + (functions.length - 1)); + } + + for (int i = 0; i < boundsAry.length; i++) { + bounds[i+1] = boundsAry[i].getFloatValue(); + } + bounds[0] = getDomain(0); + bounds[bounds.length-1] = getDomain(1); + + // read the encode array (required) + PDFObject encodeObj = obj.getDictRef("Encode"); + if (encodeObj == null) { + throw new PDFParseException("Encode required for function type 3!"); + } + PDFObject[] encodeAry = encodeObj.getArray(); + encode = new float[encodeAry.length]; + if (encode.length != 2*functions.length) { + throw new PDFParseException("Encode array must be of length " + 2*functions.length); + } + for (int i = 0; i < encodeAry.length; i++) { + encode[i] = encodeAry[i].getFloatValue(); + } + } + + /** + * + * @param inputs an array of 1 input values + * @param outputs an array of size n which will be filled + * with the output values, or null to return a new array + */ + @Override + protected void doFunction(float[] inputs, int inputOffset, + float[] outputs, int outputOffset) { + + float x = inputs[inputOffset]; + + // calculate the output values + int p = bounds.length - 2; + while (x < bounds[p]) p--; + x = interpolate(x, bounds[p], bounds[p+1], encode[2*p], encode[2*p + 1]); + float[] out = functions[p].calculate(new float[]{x}); + for (int i = 0; i < out.length; i++) { + outputs[i + outputOffset] = out[i]; + } + } + + @Override + public int getNumInputs() { + return 1; + } + + @Override + public int getNumOutputs() { + return functions[0].getNumOutputs(); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/FunctionType4.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/FunctionType4.java new file mode 100644 index 0000000000..b54f066ba9 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/FunctionType4.java @@ -0,0 +1,121 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.function; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.*; + +import com.github.librepdf.pdfrenderer.function.postscript.PostScriptParser; +import com.github.librepdf.pdfrenderer.function.postscript.operation.OperationSet; +import com.github.librepdf.pdfrenderer.function.postscript.operation.PostScriptOperation; +import com.github.librepdf.pdfrenderer.PDFObject; + +/** + *

    A PostScript function is represented as a stream containing code + * written in a small subset of the PostScript language. + * This reference is taken from the (3200-1:2008:7.10.5)

    + * + * http://www.adobe.com/devnet/acrobat/pdfs/adobe_supplement_iso32000.pdf + *

    + */ +public class FunctionType4 extends PDFFunction { + + /** the list of tokens and sub-expressions. */ + private List tokens; + + /** the stack of operations. The stack contents should all be Comparable. */ + private Stack stack; + + /** Creates a new instance of FunctionType4 */ + protected FunctionType4() { + super(TYPE_4); + } + + /** Read the function information from a PDF Object */ + @Override + protected void parse(PDFObject obj) throws IOException { + ByteBuffer buf = obj.getStreamBuffer(); + + byte[] byteA = new byte[buf.remaining()]; + buf.get(byteA); + String scriptContent = new String(byteA, "UTF-8"); + this.tokens = new PostScriptParser().parse(scriptContent); + } + + /** + * Map from m input values to n output values. + * The number of inputs m must be exactly one half the size of the + * domain. The number of outputs should match one half the size of the + * range. + * + * @param inputs an array of m input values + * @param inputOffset the offset into the input array to read from + * @param outputs an array of size >= n which will be filled + * with the output values + * @param outputOffset the offset into the output array to write to + */ + @Override + protected void doFunction(float[] inputs, int inputOffset, float[] outputs, int outputOffset) { + prepareInitialStack(inputs, inputOffset); + for (Iterator iterator = this.tokens.iterator(); iterator.hasNext(); ) { + String token = iterator.next(); + PostScriptOperation op = OperationSet.getInstance().getOperation(token); + op.eval(this.stack); + } + assertResultIsCorrect(outputs, outputOffset); + prepareResult(outputs, outputOffset); + } + + /************************************************************************* + * @param outputs + * @param outputOffset + ************************************************************************/ + private void prepareResult(float[] outputs, int outputOffset) { + for (int i = outputOffset; i < outputs.length; i++) { + outputs[outputs.length-i-1] = ((Double)this.stack.pop()).floatValue(); + } + } + + /************************************************************************* + * Put all input values on the initial stack. + * All values are pushed as Double because we calculate internally with double. + * @param inputs + * @param inputOffset + ************************************************************************/ + + private void prepareInitialStack(float[] inputs, int inputOffset) { + this.stack = new Stack(); + for (int i = inputOffset; i < inputs.length; i++) { + this.stack.push(new Double(inputs[i])); + } + } + + /************************************************************************* + * @param outputs + * @param outputOffset + ************************************************************************/ + + private void assertResultIsCorrect(float[] outputs, int outputOffset) { + int expectedResults = outputs.length-outputOffset; + if (this.stack.size() != expectedResults) { + throw new IllegalStateException("Output does not match result "+expectedResults+"/"+this.stack); + } + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/PDFFunction.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/PDFFunction.java new file mode 100644 index 0000000000..eb93ba84e6 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/PDFFunction.java @@ -0,0 +1,347 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +package com.github.librepdf.pdfrenderer.function; + +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/** + *

    PDF Functions are defined in the reference as Section 3.9.

    + * + *

    A PDF function maps some set of m inputs into some set + * of n outputs. There are 4 types of functions: + *

    • Type 0: Sampled functions. (PDF 1.2)
      + * A sampled function (type 0) uses a table of sample values + * to define the function. Various techniques are used to + * interpolate values between the sample values + * (see Section 3.9.1, "Type 0 (Sampled) Functions").
    • + *
    • Type 2: Exponential Interpolation. (PDF 1.3)
      + * An exponential interpolation function (type 2) + * defines a set of coefficients for an exponential function + * (see Section 3.9.2, + * "Type 2 (Exponential Interpolation) Functions").
    • + *
    • Type 3: Stitching functions. (PDF 1.3)
      + * A stitching function (type 3) is a combination of + * other functions, partitioned across a domain + * (see Section 3.9.3, "Type 3 (Stitching) Functions").
    • + *
    • Type 4: Postscript calculations. (PDF 1.3)
      + * A PostScript calculator function (type 4) uses operators + * from the PostScript language to describe an arithmetic + * expression (see Section 3.9.4, + * "Type 4 (PostScript Calculator) Functions").
    • + *
    + *

    + * + *

    + * The function interface contains a single method, calculate which + * takes an array of m floats an interprets them into an array of + * n floats. + *

    + * PDFFunctions do not have accessible constructors. Instead, use the + * static getFunction() method to read a functions from a PDF Object. + * + */ +public abstract class PDFFunction { + + /** Sampled function */ + public static final int TYPE_0 = 0; + + /** Exponential interpolation function */ + public static final int TYPE_2 = 2; + + /** Stitching function. */ + public static final int TYPE_3 = 3; + + /** PostScript calculator function. */ + public static final int TYPE_4 = 4; + + /** the type of this function from the list of known types */ + private final int type; + + /** the input domain of this function, an array of 2 * m floats */ + private float[] domain; + + /** the output range of this functions, and array of 2 * n floats. + * required for type 0 and 4 functions + */ + private float[] range; + + /** Creates a new instance of PDFFunction */ + protected PDFFunction (int type) { + this.type = type; + } + + /** + * Get a PDFFunction from a PDFObject + */ + public static PDFFunction getFunction (PDFObject obj) + throws IOException { + PDFFunction function; + int type; + float[] domain = null; + float[] range = null; + + // read the function type (required) + PDFObject typeObj = obj.getDictRef ("FunctionType"); + if (typeObj == null) { + throw new PDFParseException ( + "No FunctionType specified in function!"); + } + type = typeObj.getIntValue (); + + // read the function's domain (required) + PDFObject domainObj = obj.getDictRef ("Domain"); + if (domainObj == null) { + throw new PDFParseException ("No Domain specified in function!"); + } + + PDFObject[] domainAry = domainObj.getArray (); + domain = new float[domainAry.length]; + for (int i = 0; i < domainAry.length; i++) { + domain[i] = domainAry[i].getFloatValue (); + } + + // read the function's range (optional) + PDFObject rangeObj = obj.getDictRef ("Range"); + if (rangeObj != null) { + PDFObject[] rangeAry = rangeObj.getArray (); + range = new float[rangeAry.length]; + for (int i = 0; i < rangeAry.length; i++) { + range[i] = rangeAry[i].getFloatValue (); + } + } + + // now create the acual function object + switch (type) { + case TYPE_0: + if (rangeObj == null) { + throw new PDFParseException ( + "No Range specified in Type 0 Function!"); + } + function = new FunctionType0 (); + break; + case TYPE_2: + function = new FunctionType2 (); + break; + case TYPE_3: + function = new FunctionType3 (); + break; + case TYPE_4: + if (rangeObj == null) { + throw new PDFParseException ( + "No Range specified in Type 4 Function!"); + } + function = new FunctionType4 (); + break; + default: + throw new PDFParseException ( + "Unsupported function type: " + type); + } + + // fill in the domain and optionally the range + function.setDomain (domain); + if (range != null) { + function.setRange (range); + } + + // now initialize the function + function.parse (obj); + + return function; + } + + /** + * Perform a linear interpolation. Given a value x, and two points, + * (xmin, ymin), (xmax, ymax), where xmin <= x <= xmax, calculate a value + * y on the line from (xmin, ymin) to (xmax, ymax). + * + * @param x the x value of the input + * @param xmin the minimum x value + * @param ymin the minimum y value + * @param xmax the maximum x value + * @param ymax the maximum y value + * @return the y value interpolated from the given x + */ + public static float interpolate(float x, float xmin, float xmax, + float ymin, float ymax) { + float value = (ymax - ymin) / (xmax - xmin); + value *= x - xmin; + value += ymin; + + return value; + } + + /** + * Get the type of this function + * + * @return one of the types of function (0-4) + */ + public int getType () { + return this.type; + } + + /** + * Get the number of inputs, m, required by this function + * + * @return the number of input values expected by this function + */ + public int getNumInputs () { + return (this.domain.length / 2); + } + + /** + * Get the number of outputs, n, returned by this function + * + * @return the number of output values this function will return + */ + public int getNumOutputs () { + if (this.range == null) { + return 0; + } + return (this.range.length / 2); + } + + /** + * Get a component of the domain of this function + * + * @param i the index into the domain array, which has size 2 * m. + * the ith entry in the array has index 2i, + * 2i + 1 + * @return the ith entry in the domain array + */ + protected float getDomain (int i) { + return this.domain[i]; + } + + /** + * Set the domain of this function + */ + protected void setDomain (float[] domain) { + this.domain = domain; + } + + /** + * Get a component of the range of this function + * + * @param i the index into the range array, which has size 2 * n. + * the ith entry in the array has index 2i, + * 2i + 1 + * @return the ith entry in the range array + */ + protected float getRange (int i) { + if (this.range == null) { + if ((i % 2) == 0) { + return Float.MIN_VALUE; + } else { + return Float.MAX_VALUE; + } + } + return this.range[i]; + } + + /** + * Set the range of this function + */ + protected void setRange (float[] range) { + this.range = range; + } + + /** + * Map from m input values to n output values. + * The number of inputs m must be exactly one half the size of the + * domain. The number of outputs should match one half the size of the + * range. + * + * @param inputs an array of >= m input values + * @return the array of n output values + */ + public float[] calculate (float[] inputs) { + float[] outputs = new float[getNumOutputs ()]; + calculate (inputs, 0, outputs, 0); + return outputs; + } + + /** + * Map from m input values to n output values. + * The number of inputs m must be exactly one half the size of the + * domain. The number of outputs should match one half the size of the + * range. + * + * @param inputs an array of >= m input values + * @param inputOffset the offset into the input array to read from + * @param outputs an array of size >= n which will be filled + * with the output values + * @param outputOffset the offset into the output array to write to + * @return the array of n output values + */ + public float[] calculate (float[] inputs, int inputOffset, + float[] outputs, int outputOffset) { + // check the inputs + if (inputs.length - inputOffset < getNumInputs ()) { + throw new IllegalArgumentException ( + "Wrong number of inputs to function!"); + } + + // check the outputs + if (this.range != null && outputs.length - outputOffset < getNumOutputs ()) { + throw new IllegalArgumentException ( + "Wrong number of outputs for function!"); + } + + // clip the inputs to domain + for (int i = 0; i < inputs.length; i++) { + // clip to the domain -- min(max(x, domain<2i>), domain<2i+1>) + inputs[i] = Math.max (inputs[i], getDomain (2 * i)); + inputs[i] = Math.min (inputs[i], getDomain ((2 * i) + 1)); + } + + // do the actual calculation + doFunction (inputs, inputOffset, outputs, outputOffset); + + // clip the outputs to range + for (int i = 0; this.range != null && i < outputs.length; i++) { + // clip to range -- min(max(r, range<2i>), range<2i + 1>) + outputs[i] = Math.max (outputs[i], getRange (2 * i)); + outputs[i] = Math.min (outputs[i], getRange ((2 * i) + 1)); + } + + return outputs; + } + + /** + * Subclasses must implement this method to perform the actual function + * on the given set of data. Note that the inputs are guaranteed to be + * clipped to the domain, while the outputs will be automatically clipped + * to the range after being returned from this function. + * + * @param inputs guaranteed to be at least as big as + * getNumInputs() and all values within range + * @param inputOffset the offset into the inputs array to read from + * @param outputs guaranteed to be at least as big as + * getNumOutputs(), but not yet clipped to domain + * @param outputOffset the offset into the output array to write to + */ + protected abstract void doFunction (float[] inputs, int inputOffset, + float[] outputs, int outputOffset); + + /** Read the function information from a PDF Object */ + protected abstract void parse (PDFObject obj) throws IOException; +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/PostScriptParser.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/PostScriptParser.java new file mode 100644 index 0000000000..bb20a7c07a --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/PostScriptParser.java @@ -0,0 +1,69 @@ +package com.github.librepdf.pdfrenderer.function.postscript; + +import java.util.LinkedList; +import java.util.List; +import java.util.StringTokenizer; + +/***************************************************************************** + * Very simple post script parser / tokenizer + * + * @author Bernd Rosstauscher + * @since 22.10.2010 + ****************************************************************************/ + +public class PostScriptParser { + + /************************************************************************* + * Constructor + ************************************************************************/ + + public PostScriptParser() { + super(); + } + + /************************************************************************* + * Parses the given script and returns a list of tokens. + * @param scriptContent to parse. + * @return the list of tokens. + ************************************************************************/ + + public List parse(String scriptContent) { + List tokens = new LinkedList(); + StringTokenizer tok = new StringTokenizer(scriptContent, " \t\n\r"); + while (tok.hasMoreTokens()) { + String t = tok.nextToken(); + t = filterBlockStart(t); + t = filterBlockEnd(t); + if (t.length() > 0) { + tokens.add(t.trim()); + } + } + return tokens; + } + + /************************************************************************* + * @param t + * @return + ************************************************************************/ + private String filterBlockEnd(String t) { + if (t.endsWith("}")) { + t = t.substring(0, t.length()-1); + } + return t; + } + + /************************************************************************* + * @param t + * @return + ************************************************************************/ + private String filterBlockStart(String t) { + if (t.startsWith("{")) { + t = t.substring(1); + } + return t; + } + + + +} + diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Abs.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Abs.java new file mode 100644 index 0000000000..a44e0be5a3 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Abs.java @@ -0,0 +1,19 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + +final class Abs implements PostScriptOperation { + @Override + /** + * num1 abs num2

    + * + * The type of the result is the same as the type of num1, + * unless num1 is the smallest (most negative) integer, + * in which case the result is a real number.

    + * + * errors: stackunderflow, typecheck + */ + public void eval(Stack environment) { + environment.push(Math.abs((Double)environment.pop())); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Add.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Add.java new file mode 100644 index 0000000000..c5a3c7ad0b --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Add.java @@ -0,0 +1,19 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + +final class Add implements PostScriptOperation { + @Override + /** + * num1 num2 add sum

    + * + * If both operands are integers and the result is + * within integer range, the result is an integer; + * otherwise, the result is a real number.

    + * + * errors: stackunderflow, typecheck, undefinedresult + */ + public void eval(Stack environment) { + environment.push((Double)environment.pop() + (Double)environment.pop()); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/And.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/And.java new file mode 100644 index 0000000000..a9255b603d --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/And.java @@ -0,0 +1,20 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + +final class And implements PostScriptOperation { + @Override + /** + * bool1|int1 bool2|int2 and bool3|int3

    + * + * returns the logical conjunction of the operands + * if they are boolean. If the operands are integers, + * and returns the bitwise "and" of their binary + * representations.

    + * + * errors: stackunderflow, typecheck + */ + public void eval(Stack environment) { + environment.push((Long)environment.pop() & (Long)environment.pop()); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Atan.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Atan.java new file mode 100644 index 0000000000..8123f8d412 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Atan.java @@ -0,0 +1,30 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + + +final class Atan implements PostScriptOperation { + @Override + /** + * num den atan angle

    + * + * returns the angle (in degress between + * 0 and 360) whose tangent is num divided by den. + * Either num or den may be 0, but not both. The signs + * of num and den determine the quadrant in which the + * result will lie: positive num yeilds a result in the + * positive y plane, while a positive den yeilds a result in + * the positive x plane. The result is a real number.

    + * + * errors: stackunderflow, typecheck, undefinedresult + */ + public void eval(Stack environment) { + double den = (Double)environment.pop(); + double num = (Double)environment.pop(); + if (den == 0.0) { + environment.push(90.0); + } else { + environment.push(Math.toDegrees(Math.atan(num / den))); + } + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Bitshift.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Bitshift.java new file mode 100644 index 0000000000..886358e444 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Bitshift.java @@ -0,0 +1,25 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + +final class Bitshift implements PostScriptOperation { + @Override + /** + * int1 shift bitshift int2

    + * + * shifts the binary representation of int1 left by + * shift bits and returns the result. Bits shifted out + * are lost; bits shifted in are 0. If shift is negative, + * a right shift by –shift bits is performed. + * This PostScriptOperation produces an arithmetically correct + * result only for positive values of int1. + * Both int1 and shift must be integers.

    + * + * errors: stackunderflow, typecheck + */ + public void eval(Stack environment) { + long shift = (Long)environment.pop(); + long int1 = (Long)environment.pop(); + environment.push(int1 << shift); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Ceiling.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Ceiling.java new file mode 100644 index 0000000000..90127cd9e7 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Ceiling.java @@ -0,0 +1,19 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + +final class Ceiling implements PostScriptOperation { + @Override + /** + * num1 ceiling num2

    + * + * returns the least integer value greater than or equal + * to num1. The type of the result is the same as the type + * of the operand.

    + * + * errors: stackunderflow, typecheck + */ + public void eval(Stack environment) { + environment.push(Math.ceil((Double)environment.pop())); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Copy.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Copy.java new file mode 100644 index 0000000000..b35551f6c6 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Copy.java @@ -0,0 +1,64 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + final class Copy implements PostScriptOperation { + @Override + /** + * any1 ... anyn n copy any1 ... anyn any1 ... anyn + * array1 array2 copy subarray2
    + * string1 string2 copy substring2

    + * + * performs two entirely different functions, depending on the + * type of the topmost operand. + * In the first form, where the top element on the operand + * stack is a nonnegative integer n, copy pops n from the + * stack and duplicates the top n elements on the stack + * as shown above. This form of copy operates only on the + * objects themselves, not on the values of composite objects.

    + * + * Examples
    + * (a) (b) (c) 2 copy Þ (a) (b) (c) (b) (c)
    + * (a) (b) (c) 0 copy Þ (a) (b) (c)

    + * + * In the other forms, copy copies all the elements of the + * first composite object into the second. The composite + * object operands must be of the same type, except that + * a packed array can be copied into an array (and only into + * an array—copy cannot copy into packed arrays, because + * they are read-only). This form of copy copies the value of + * a composite object. This is quite different from dup and + * other operators that copy only the objects themselves + * (see Section 3.3.1, "Simple and Composite Objects"). + * However, copy performs only one level of copying. + * It does not apply recursively to elements that are + * themselves composite objects; instead, the values + * of those elements become shared. In the case of arrays or + * strings, the length of the second object must be at least as + * great as the first; copy returns the initial subarray or + * substring of the second operand into which the elements + * were copied. Any remaining elements of array2 or + * string2 are unaffected.

    + * + * Example:
    + * /a1 [1 2 3] def
    + * a1 dup length array copy Þ [1 2 3]

    + * + * errors: invalidaccess, rangecheck, stackoverflow, + * stackunderflow, typecheck + */ + public void eval(Stack environment) { + Number count = (Number) environment.pop(); +// ???? + Object[] buffer = new Object[count.intValue()]; + for (int i = 0; i < buffer.length; i++) { + buffer[i] = environment.pop(); + } + for (int i = 0; i < buffer.length; i++) { + environment.push(buffer[buffer.length-i-1]); + } + for (int i = 0; i < buffer.length; i++) { + environment.push(buffer[buffer.length-i-1]); + } + } + } + \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Cvi.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Cvi.java new file mode 100644 index 0000000000..a1b5e881e2 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Cvi.java @@ -0,0 +1,28 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; +final class Cvi implements PostScriptOperation { + @Override + /** + * num cvi int or string cvi int

    + * + * takes an integer, real, or string and produces an + * integer result. If the operand is an integer, cvi + * simply returns it. If the operand is a real number, + * it truncates any fractional part (that is, rounds + * it toward 0) and converts it to an integer. + * If the operand is a string, cvi invokes the equivalent + * of the token operator to interpret the characters + * of the string as a number according to the PostScript + * syntax rules. If that number is a real number, cvi converts + * it to an integer. + * A rangecheck error occurs if a real number is too + * large to convert to an integer.

    + * + * errors: invalidaccess, rangecheck, stackunderflow, + * syntaxError, typecheck, + */ + public void eval(Stack environment) { + environment.push(environment.pop()); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Cvr.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Cvr.java new file mode 100644 index 0000000000..2e70f65f4a --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Cvr.java @@ -0,0 +1,26 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + +final class Cvr implements PostScriptOperation { + @Override + /** + * num cvr real or string cvr real

    + * + * (convert to real) takes an integer, real, or string + * object and produces a real result. If the operand + * is an integer, cvr converts it to a real number. + * If the operand is a real number, cvr simply returns it. + * If the operand is a string, cvr invokes the equivalent + * of the token operator to interpret the characters of + * the string as a number according to the PostScript + * syntax rules. If that number is an integer, cvr converts + * it to a real number.

    + * + * errors: invalidaccess, limitcheck, stackunderflow, + * syntaxerror, typecheck, undefinedresult + */ + public void eval(Stack environment) { + // YOUR CODE IN THIS SPACE + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Div.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Div.java new file mode 100644 index 0000000000..534d468d80 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Div.java @@ -0,0 +1,22 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + +final class Div implements PostScriptOperation { + @Override + /** + * num1 num2 div quotient

    + * + * divides num1 by num2, producing a result that is + * always a real number even if both operands are integers. + * Use idiv instead if the operands are integers and an + * integer result is desired.

    + * + * errors: stackunderflow, typecheck, undefinedresult + */ + public void eval(Stack environment) { + double num2 = (Double)environment.pop(); + double num1 = (Double)environment.pop(); + environment.push(num1 / num2); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Dup.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Dup.java new file mode 100644 index 0000000000..9c92dc6e4a --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Dup.java @@ -0,0 +1,22 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + +final class Dup implements PostScriptOperation { + @Override + /** + * any dup any any

    + * + * duplicates the top element on the operand stack. + * dup copies only the object; the value of a composite + * object is not copied but is shared. + * See Section 3.3, "Data Types and Objects."

    + * + * errors: stackoverflow, stackunderflow + */ + public void eval(Stack environment) { + Object obj = environment.pop(); + environment.push(obj); + environment.push(obj); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Eq.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Eq.java new file mode 100644 index 0000000000..4b90f29751 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Eq.java @@ -0,0 +1,40 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + + +final class Eq implements PostScriptOperation { + @Override + /** + * any1 any2 eq bool

    + * + * pops two objects from the operand stack and pushes\ + * true if they are equal, or false if not. + * The definition of equality depends on the types of + * the objects being compared. + * Simple objects are equal if their types and values + * are the same. Strings are equal if their lengths and + * individual elements are equal. + * Other composite objects + * (arrays and dictionaries) are equal only if they share + * the same value. Separate values are considered unequal, + * even if all the components of those values are the + * same. + * This operator performs some type conversions. + * Integers and real numbers can be compared freely: + * an integer and a real number representing the same + * mathematical value are considered equal by eq. + * Strings and names can likewise be compared freely: + * a name defined by some sequence of characters is equal + * to a string whose elements are the same sequence of + * characters. + * The literal/executable and access attributes of + * objects are not considered in comparisons + * between objects.

    + * + * errors: invalidaccess, stackunderflow + */ + public void eval(Stack environment) { + environment.push(environment.pop().equals(environment.pop())); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Exch.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Exch.java new file mode 100644 index 0000000000..de80471dc5 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Exch.java @@ -0,0 +1,13 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + +final class Exch implements PostScriptOperation { + @Override + public void eval(Stack environment) { // any1 any2 exch any2 any1 - exchange top of stack + Object any1 = environment.pop(); + Object any2 = environment.pop(); + environment.push(any1); + environment.push(any2); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Exp.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Exp.java new file mode 100644 index 0000000000..425287973d --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Exp.java @@ -0,0 +1,23 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + +final class Exp implements PostScriptOperation { + @Override + /** + * base exponent exp real

    + * + * raises base to the exponent power. The operands may be + * either integers or real numbers. If the exponent has a + * fractional part, the result is meaningful only if the + * base is nonnegative. The result is always a real number.

    + * + * errors: stackunderflow, typecheck, undefinedresult + */ + public void eval(Stack environment) { + double exponent = (Double)environment.pop(); + double base = (Double)environment.pop(); + environment.push(Math.pow(exponent, base)); + } + +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Expression.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Expression.java new file mode 100644 index 0000000000..f8ef93fd52 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Expression.java @@ -0,0 +1,17 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.LinkedList; + + + +public class Expression extends LinkedList { + + @Override + public boolean equals(Object obj) { + if (obj instanceof Expression) { + // actually validate the list contents are the same expressions + return true; + } + return false; + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/False.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/False.java new file mode 100644 index 0000000000..346624c483 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/False.java @@ -0,0 +1,20 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + + +final class False implements PostScriptOperation { + @Override + /** + * false false

    + * + * pushes a boolean object whose value is false on the + * operand stack. false is not an operator; it is a name in + * systemdict associated with the boolean value false.

    + * + * errors: stackoverflow + */ + public void eval(Stack environment) { + environment.push(false); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Floor.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Floor.java new file mode 100644 index 0000000000..6c7016720c --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Floor.java @@ -0,0 +1,19 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + +final class Floor implements PostScriptOperation { + @Override + /** + * num1 floor num2

    + * + * returns the greatest integer value less than or equal + * to num1. The type of the result is the same as the type + * of the operand.

    + * + * errors: stackunderflow, typecheck + */ + public void eval(Stack environment) { + environment.push(Math.floor((Double)environment.pop())); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Ge.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Ge.java new file mode 100644 index 0000000000..5a6747e6ef --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Ge.java @@ -0,0 +1,30 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + + +final class Ge implements PostScriptOperation { + @Override + /** + * num1 num2 ge bool

    + * + * pops two objects from the operand stack and pushes true + * if the first operand is greater than or equal to the second, + * or false otherwise. If both operands are numbers, + * ge compares their mathematical values. If both operands + * are strings, ge compares them element by element, treating + * the elements as integers in the range 0 to 255, to determine + * whether the first string is lexically greater than or equal + * to the second. If the operands are of other types or one + * is a string and the other is a number, a typecheck + * error occurs.

    + * + * errors: invalidaccess, stackunderflow, typecheck + */ + public void eval(Stack environment) { + double num2 = (Double)environment.pop(); + double num1 = (Double)environment.pop(); + environment.push(num1 >= num2); + } +} + diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Gt.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Gt.java new file mode 100644 index 0000000000..ffdf9a216a --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Gt.java @@ -0,0 +1,28 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + + +final class Gt implements PostScriptOperation { + @Override + /** + * num1 num2 gt bool

    + * + * pops two objects from the operand stack and pushes true + * if the first operand is greater than the second, or + * false otherwise. If both operands are numbers, gt compares + * their mathematical values. If both operands are strings, + * gt compares them element by element, treating the elements + * as integers in the range 0 to 255, to determine whether + * the first string is lexically greater than the second. + * If the operands are of other types or one is a string + * and the other is a number, a typecheck error occurs.

    + * + * errors: invalidaccess, stackunderflow, typecheck + */ + public void eval(Stack environment) { + double num2 = (Double)environment.pop(); + double num1 = (Double)environment.pop(); + environment.push(num1 > num2); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Idiv.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Idiv.java new file mode 100644 index 0000000000..0797c20842 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Idiv.java @@ -0,0 +1,22 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + +final class Idiv implements PostScriptOperation { + @Override + /** + * int1 int2 idiv quotient

    + * + * divides int1 by int2 and returns the integer part + * of the quotient, with any fractional part discarded. + * Both operands of idiv must be integers and the result + * is an integer.

    + * + * stackunderflow, typecheck, undefinedresult + */ + public void eval(Stack environment) { + long int2 = (Long)environment.pop(); + long int1 = (Long)environment.pop(); + environment.push(int1 / int2); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/If.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/If.java new file mode 100644 index 0000000000..e5cb0e0d42 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/If.java @@ -0,0 +1,28 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + + +final class If implements PostScriptOperation { + @Override + /** + * bool {proc} if -

    + * + * removes both operands from the stack, then executes proc + * if bool is true. The if operator pushes no results of + * its own on the operand stack, but proc may do so (see + * Section 3.5, "Execution").

    + * + * Examples

    + * 3 4 lt {(3 is less than 4)} if

    + * + * errors: stackunderflow, typecheck + */ + public void eval(Stack environment) { + if ((Boolean)environment.pop()) { + environment.push(environment.pop()); + } else { + environment.pop(); + } + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/IfElse.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/IfElse.java new file mode 100644 index 0000000000..b72340e906 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/IfElse.java @@ -0,0 +1,32 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + +final class IfElse implements PostScriptOperation { + @Override + /** + * bool {expr1} {expr2} ifelse -

    + * + * removes all three operands from the stack, then + * executes proc1 if bool is true or proc2 if bool is false. + * The ifelse operator pushes no results of its own on the + * operand stack, but the procedure it executes may do so + * (see Section 3.5, "Execution").

    + * + * Examples

    + * 4 3 lt {(TruePart)} {(FalsePart)} ifelse
    + * results in FalsePart, since 4 is not less than 3

    + * + * errors: stackunderflow, typecheck + */ + public void eval(Stack environment) { + // execute expr1 if bool is true, expr2 if false + if ((Boolean)environment.pop()) { +// expression.push(popExpression()); + environment.pop(); + } else { + environment.pop(); +// expression.push(popExpression()); + } + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Index.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Index.java new file mode 100644 index 0000000000..b6cc8b71fb --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Index.java @@ -0,0 +1,11 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + +final class Index implements PostScriptOperation { + @Override + public void eval(Stack environment) { // anyn ... any0 n index anyn ... any0 anyn + long n = Math.round((Double)environment.pop()); + environment.push(environment.get((int)(environment.size() - n - 1))); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Le.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Le.java new file mode 100644 index 0000000000..de204ede83 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Le.java @@ -0,0 +1,29 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + +final class Le implements PostScriptOperation { + @Override + /** + * num1 num2 le bool

    + * + * pops two objects from the operand stack and pushes true + * if the first operand is less than or equal to the second, + * or false otherwise. If both operands are numbers, le + * compares their mathematical values. If both operands are + * strings, le compares them element by element, treating + * the elements as integers in the range 0 to 255, + * to determine whether the first string is lexically less + * than or equal to the second. If the operands are of other + * types or one is a string and the other is a number, a + * typecheck error occurs.

    + * + * errors: invalidaccess, stackunderflow, typecheck + */ + public void eval(Stack environment) { + double num2 = (Double)environment.pop(); + double num1 = (Double)environment.pop(); + environment.push(num1 <= num2); + } +} + diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Ln.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Ln.java new file mode 100644 index 0000000000..ba000b10cd --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Ln.java @@ -0,0 +1,19 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + + +final class Ln implements PostScriptOperation { + @Override + /** + * num ln real

    + * + * returns the natural logarithm (base e) of num. + * The result is a real number.

    + * + * errors: rangecheck, stackunderflow, typecheck + */ + public void eval(Stack environment) { + environment.push(Math.log((Double)environment.pop())); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Log.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Log.java new file mode 100644 index 0000000000..e63a125357 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Log.java @@ -0,0 +1,19 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + + +final class Log implements PostScriptOperation { + @Override + /** + * num log real

    + * + * returns the common logarithm (base 10) of num. + * The result is a real number.

    + * + * errors: rangecheck, stackunderflow, typecheck + */ + public void eval(Stack environment) { + environment.push(Math.log10((Double)environment.pop())); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Lt.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Lt.java new file mode 100644 index 0000000000..1029c4d475 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Lt.java @@ -0,0 +1,27 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + +final class Lt implements PostScriptOperation { + @Override + /** + * num1 num2 lt bool

    + * + * pops two objects from the operand stack and pushes true + * if the first operand is less than the second, or false + * otherwise. If both operands are numbers, lt compares + * their mathematical values. If both operands are strings, + * lt compares them element by element, treating the elements + * as integers in the range 0 to 255, to determine whether + * the first string is lexically less than the second. + * If the operands are of other types or one is a string + * and the other is a number, a typecheck error occurs.

    + * + * errors: invalidaccess, stackunderflow, typecheck + */ + public void eval(Stack environment) { + double num2 = (Double)environment.pop(); + double num1 = (Double)environment.pop(); + environment.push(num1 < num2); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Mod.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Mod.java new file mode 100644 index 0000000000..fdd5d59553 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Mod.java @@ -0,0 +1,24 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + + +final class Mod implements PostScriptOperation { + @Override + /** + * int1 int2 mod remainder

    + * + * returns the remainder that results from + * dividing int1 by int2. The sign of the result + * is the same as the sign of the dividend int1. + * Both operands must be integers and the result + * is an integer.

    + * + * errors: stackunderflow, typecheck, undefinedresult + */ + public void eval(Stack environment) { + long int2 = (Long)environment.pop(); + long int1 = (Long)environment.pop(); + environment.push(int1 % int2); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Mul.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Mul.java new file mode 100644 index 0000000000..eafe732677 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Mul.java @@ -0,0 +1,21 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + + +final class Mul implements PostScriptOperation { + @Override + /** + * num1 num2 mul product

    + * + * returns the product of num1 and num2. + * If both operands are integers and the result + * is within integer range, the result is an integer; + * otherwise, the result is a real number.

    + * + * errors: stackunderflow, typecheck, undefinedresult + */ + public void eval(Stack environment) { + environment.push((Double)environment.pop() * (Double)environment.pop()); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Ne.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Ne.java new file mode 100644 index 0000000000..2029c6ae92 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Ne.java @@ -0,0 +1,21 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + + +final class Ne implements PostScriptOperation { + @Override + /** + * any1 any2 ne bool

    + * + * pops two objects from the operand stack and pushes false + * if they are equal, or true if not. What it means for objects + * to be equal is presented in the description of the + * eq operator.

    + * + * errors: invalidaccess, stackunderflow + */ + public void eval(Stack environment) { + environment.push(!environment.pop().equals(environment.pop())); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Neg.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Neg.java new file mode 100644 index 0000000000..b25a6c29f4 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Neg.java @@ -0,0 +1,21 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + + +final class Neg implements PostScriptOperation { + @Override + /** + * num1 neg num2

    + * + * returns the negative of num1. The type of the result + * is the same as the type of num1 unless num1 is the + * smallest (most negative) integer, in which case the + * result is a real number.

    + * + * errors: stackunderflow, typecheck + */ + public void eval(Stack environment) { + environment.push(-(Double)environment.pop()); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Not.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Not.java new file mode 100644 index 0000000000..8f825f0003 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Not.java @@ -0,0 +1,21 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + + +final class Not implements PostScriptOperation { + @Override + /** + * bool1|int1 not bool2|int2

    + * + * returns the logical negation of the operand if it is + * boolean. If the operand is an integer, not returns the + * bitwise complement (ones complement) of its binary + * representation.

    + * + * errors: stackunderflow, typecheck + */ + public void eval(Stack environment) { + environment.push(~(Long)environment.pop()); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/OperationSet.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/OperationSet.java new file mode 100644 index 0000000000..13e961731b --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/OperationSet.java @@ -0,0 +1,115 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.HashMap; +import java.util.Map; + +public class OperationSet { + + /** the set of all Operations we support. These operations are defined + * in Appendix B - Operators.*/ + private Map operationSet = null; + + private static OperationSet instance; + + /************************************************************************* + * Constructor + ************************************************************************/ + + private OperationSet() { + super(); + initOperations(); + } + + /************************************************************************* + * @return + ************************************************************************/ + + public static synchronized OperationSet getInstance() { + if (instance == null) { + instance = new OperationSet(); + } + return instance; + } + + /************************************************************************* + * @param token + * @return + ************************************************************************/ + + public PostScriptOperation getOperation(String token) { + PostScriptOperation result = this.operationSet.get(token.trim().toLowerCase()); + if (result == null) { + result = new PushAsNumber(token); + } + return result; + + } + + + /** + * Initialize the operations that we can perform. + */ + private void initOperations() { + /** these operators consider the left hand arguments as deeper in + * the stack than the right hand arguments, thus the right-hand is + * is the top of the stack and is popped first. + * + * PostScriptOperation details in PostScript Language Reference Manual: + * http://www.adobe.com/products/postscript/pdfs/PLRM.pdf + * Chapter 8 - Operator Details + */ + if (this.operationSet == null) { + this.operationSet = new HashMap(); + + // Arithmetic Operators + this.operationSet.put("abs", new Abs()); + this.operationSet.put("add", new Add()); + this.operationSet.put("atan", new Atan()); + this.operationSet.put("ceiling", new Ceiling()); + this.operationSet.put("cvi", new Cvi()); + this.operationSet.put("cvr", new Cvr()); + this.operationSet.put("div", new Div()); + this.operationSet.put("exp", new Exp()); + this.operationSet.put("floor", new Floor()); + this.operationSet.put("idiv", new Idiv()); + this.operationSet.put("ln", new Ln()); + this.operationSet.put("log", new Log()); + this.operationSet.put("mod", new Mod()); + this.operationSet.put("mul", new Mul()); + this.operationSet.put("neg", new Neg()); + this.operationSet.put("round", new Round()); + this.operationSet.put("sin", new Sin()); + this.operationSet.put("sqrt", new Sqrt()); + this.operationSet.put("sub", new Sub()); + this.operationSet.put("truncate", new Truncate()); + + // Relational, boolean, and bitwise operators + this.operationSet.put("and", new And()); + this.operationSet.put("bitshift", new Bitshift()); + this.operationSet.put("eq", new Eq()); + this.operationSet.put("false", new False()); + this.operationSet.put("ge", new Ge()); + this.operationSet.put("gt",new Gt()); + this.operationSet.put("le", new Le()); + this.operationSet.put("lt", new Lt()); + this.operationSet.put("ne", new Ne()); + this.operationSet.put("not", new Not()); + this.operationSet.put("or", new Or()); + this.operationSet.put("true", new True()); + this.operationSet.put("xor", new Xor()); + + // Conditional Operators + this.operationSet.put("if", new If()); + this.operationSet.put("ifelse", new IfElse()); + + // Stack Operators + this.operationSet.put("copy", new Copy()); + this.operationSet.put("dup", new Dup()); + this.operationSet.put("exch", new Exch()); + this.operationSet.put("index", new Index()); + this.operationSet.put("pop", new Pop()); + this.operationSet.put("roll", new Roll()); + } + } + +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Or.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Or.java new file mode 100644 index 0000000000..6e348ad4e5 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Or.java @@ -0,0 +1,20 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + + +final class Or implements PostScriptOperation { + @Override + /** + * bool1|int1 bool2|int2 or bool3|int3

    + * + * returns the logical disjunction of the operands if they + * are boolean. If the operands are integers, or returns + * the bitwise "inclusive or" of their binary representations.

    + * + * errors: stackunderflow, typecheck + */ + public void eval(Stack environment) { + environment.push((Long)environment.pop() | (Long)environment.pop()); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Pop.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Pop.java new file mode 100644 index 0000000000..c912b9fabb --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Pop.java @@ -0,0 +1,12 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + + +final class Pop implements PostScriptOperation { + @Override + public void eval(Stack environment) { // discard top element + environment.pop(); + } +} + diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/PostScriptOperation.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/PostScriptOperation.java new file mode 100644 index 0000000000..e9528a4e5f --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/PostScriptOperation.java @@ -0,0 +1,13 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + +public interface PostScriptOperation { + + /** + * evaluate the function, popping the stack as needed and pushing results. + */ + public void eval(Stack environment); + +} + diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/PushAsNumber.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/PushAsNumber.java new file mode 100644 index 0000000000..3387b427ad --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/PushAsNumber.java @@ -0,0 +1,35 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + + + +final class PushAsNumber implements PostScriptOperation { + + private String token; + + /************************************************************************* + * Constructor + * @param numberToken + ************************************************************************/ + + public PushAsNumber(String numberToken) { + super(); + this.token = numberToken; + } + + /************************************************************************* + * eval + * @see PostScriptOperation#eval(java.util.Stack) + ************************************************************************/ + @Override + public void eval(Stack environment) { + try { + double number = Double.parseDouble(this.token); + environment.push(number); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("PS token is not supported "+this.token); + } } + +} + diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Roll.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Roll.java new file mode 100644 index 0000000000..47f375d33e --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Roll.java @@ -0,0 +1,50 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + +final class Roll implements PostScriptOperation { + + public static int popAsInteger(Stack st) { + Object e = st.pop(); + if (e instanceof Double) { + double doubleVal = (Double) e; + return (int) doubleVal; + } else { + // error + return 0; + } + } + + @Override + public void eval(Stack environment) { + // anyn-1 ... any0 n j roll any(j-1)mod n ... anyn-1 ... any + // Roll n elements up j times + int j = popAsInteger(environment); + int n = popAsInteger(environment); + Object[] temp = new Object[n]; + + if (environment.size() < n) { + // error, cause by non-standard PS cmd, do nothing for compatibility + return; + } + + if (j >= 0) { + j %= n; + } else { + j = -j % n; + if (j != 0) + j = n - j; + } + for (int i = 0; i < n; ++i) { + temp[i] = environment.pop(); + } + + for (int i = j - 1; i > -1; --i) { + environment.push(temp[i]); + } + for (int i = n - 1; i > j - 1; --i) { + environment.push(temp[i]); + } + } + +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Round.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Round.java new file mode 100644 index 0000000000..51d189cac9 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Round.java @@ -0,0 +1,21 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + +final class Round implements PostScriptOperation { + @Override + /** + * num1 round num2

    + * + * returns the integer value nearest to num1. + * If num1 is equally close to its two nearest + * integers, round returns the greater of the two. + * The type of the result is the same as + * the type of the operand.

    + * + * errors: stackunderflow, typecheck + */ + public void eval(Stack environment) { + environment.push(Math.round((Double)environment.pop())); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Sin.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Sin.java new file mode 100644 index 0000000000..0c7b9ad73e --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Sin.java @@ -0,0 +1,20 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + + +final class Sin implements PostScriptOperation { + @Override + /** + * angle sin real

    + * + * returns the sine of angle, which is interpreted as an + * angle in degrees. The result is a real number.

    + * + * errors: stackunderflow, typecheck + */ + public void eval(Stack environment) { + double radians = Math.toRadians((Double)environment.pop()); + environment.push(Math.toDegrees(Math.sin(radians))); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Sqrt.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Sqrt.java new file mode 100644 index 0000000000..b46842396a --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Sqrt.java @@ -0,0 +1,19 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + + +final class Sqrt implements PostScriptOperation { + @Override + /** + * num sqrt real

    + * + * returns the square root of num, which must be a + * nonnegative number. The result is a real number.

    + * + * errors: rangecheck, stackunderflow, typecheck + */ + public void eval(Stack environment) { + environment.push(Math.sqrt((Double)environment.pop())); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Sub.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Sub.java new file mode 100644 index 0000000000..f0a6030168 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Sub.java @@ -0,0 +1,23 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + + +final class Sub implements PostScriptOperation { + @Override + /** + * num1 num2 sub difference

    + * + * returns the result of subtracting num2 from num1. + * If both operands are integers and the result is within + * integer range, the result is an integer; otherwise, + * the result is a real number.

    + * + * errors: stackunderflow, typecheck, undefinedresult + */ + public void eval(Stack environment) { + double num2 = (Double)environment.pop(); + double num1 = (Double)environment.pop(); + environment.push(num1 - num2); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/True.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/True.java new file mode 100644 index 0000000000..4e5664ce22 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/True.java @@ -0,0 +1,20 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + + +final class True implements PostScriptOperation { + @Override + /** + * true true

    + * + * pushes a boolean object whose value is true on the operand + * stack. true is not an operator; it is a name in systemdict + * associated with the boolean value true.

    + * + * errors: stackoverflow + */ + public void eval(Stack environment) { + environment.push(true); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Truncate.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Truncate.java new file mode 100644 index 0000000000..50e7b58e98 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Truncate.java @@ -0,0 +1,21 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + + +final class Truncate implements PostScriptOperation { + @Override + /** + * num1 truncate num2

    + * + * truncates num1 toward 0 by removing its fractional part. + * The type of the result is the same as the type of the + * operand.

    + * + * errors: stackunderflow, typecheck + */ + public void eval(Stack environment) { + double num1 = (Double)environment.pop(); + environment.push((((long) num1) - num1)); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Xor.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Xor.java new file mode 100644 index 0000000000..8d96804fa8 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/function/postscript/operation/Xor.java @@ -0,0 +1,19 @@ +package com.github.librepdf.pdfrenderer.function.postscript.operation; + +import java.util.Stack; + +final class Xor implements PostScriptOperation { + @Override + /** + * bool1|int1 bool2|int2 xor bool3|int3

    + * + * returns the logical "exclusive or" of the operands if they + * are boolean. If the operands are integers, xor returns the + * bitwise "exclusive or" of their binary representations.

    + * + * errors: stackunderflow, typecheck + */ + public void eval(Stack environment) { + environment.push((Long)environment.pop() ^ (Long)environment.pop()); + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/pattern/DummyShader.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/pattern/DummyShader.java new file mode 100644 index 0000000000..3b12353c86 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/pattern/DummyShader.java @@ -0,0 +1,25 @@ +package com.github.librepdf.pdfrenderer.pattern; + +import java.awt.Color; +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFPaint; + +public class DummyShader extends PDFShader { + + protected DummyShader(int type) { + super(type); + } + + @Override + public void parse(PDFObject shareObj) throws IOException { + + } + + @Override + public PDFPaint getPaint() { + return PDFPaint.getPaint(Color.PINK); + } + +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/pattern/PDFPattern.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/pattern/PDFPattern.java new file mode 100644 index 0000000000..f805803357 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/pattern/PDFPattern.java @@ -0,0 +1,136 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.pattern; + + +import java.awt.geom.AffineTransform; +import java.io.IOException; +import java.util.Map; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFPaint; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/** + * The abstract superclass of all PDF Pattern types + * + */ +public abstract class PDFPattern { + + /** the pattern type (1 or 2) */ + private int type; + + /** the matrix to transform from pattern space to PDF space */ + private AffineTransform xform; + + /** Creates a new instance of PDFPattern */ + protected PDFPattern(int type) + { + this.type = type; + } + + /** + * Read a pattern from the given pattern stream + */ + public static PDFPattern getPattern(PDFObject patternObj, Map resources) + throws IOException + { + // see if the pattern is already cached + PDFPattern pattern = (PDFPattern) patternObj.getCache(); + if (pattern != null) { + return pattern; + } + + // get the pattern type + int type = patternObj.getDictRef("PatternType").getIntValue(); + + // read the pattern transform matrix + PDFObject matrix = patternObj.getDictRef("Matrix"); + AffineTransform xform = null; + if (matrix == null) { + xform = new AffineTransform(); + } else { + float elts[]= new float[6]; + for (int i = 0; i < elts.length; i++) { + elts[i] = matrix.getAt(i).getFloatValue(); + } + + xform = new AffineTransform(elts); + } + + switch (type) { + case 1: + pattern = new PatternType1(); + break; + case 2: + pattern = new PatternType2(); + break; + default: + throw new PDFParseException("Unknown pattern type " + type); + } + + // set the transform + pattern.setTransform(xform); + + // parse the pattern-specific data + pattern.parse(patternObj, resources); + + // set the cache + patternObj.setCache(pattern); + + return pattern; + } + + /** + * Get the type of this pattern + */ + public int getPatternType() { + return this.type; + } + + /** + * Get the transform associated with this pattern + */ + public AffineTransform getTransform() { + return this.xform; + } + + /** + * Set the transform associated with this pattern + */ + protected void setTransform(AffineTransform xform) { + this.xform = xform; + } + + /** + * Parse the pattern-specific information from the pdf object + * + * @param patternObj the pdfobject with data for this pattern + */ + protected abstract void parse(PDFObject patternObj, Map resources) + throws IOException; + + /** + * Returns paint that represents the selected pattern + * + * @param basePaint the background paint color, or null for none + */ + public abstract PDFPaint getPaint(PDFPaint basePaint); +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/pattern/PDFShader.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/pattern/PDFShader.java new file mode 100644 index 0000000000..dc570c916a --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/pattern/PDFShader.java @@ -0,0 +1,243 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.pattern; + +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.util.Map; + +import com.github.librepdf.pdfrenderer.colorspace.PDFColorSpace; +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFPaint; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/** + *

    A PDFShader fills a given region with a shading, such as a gradient.

    + * + *

    + * Shading Dictionaries (section 4.6)
    + * A shading dictionary specifies details of a particular gradient fill, + * including the type of shading to be used, the geometry of the area to + * be shaded, and the geometry of the gradient fill. Various shading types + * are available, depending on the value of the dictionary’s ShadingType entry: + * + *

  • Function-based shadings (type 1) define the color of every point in the + * domain using a mathematical function (not necessarily smooth or continuous).
  • + * + *
  • Axial shadings (type 2) define a color blend along a line between two + * points, optionally extended beyond the boundary points by continuing + * the boundary colors.
  • + * + *
  • Radial shadings (type 3) define a blend between two circles, + * optionally extended beyond the boundary circles by continuing the + * boundary colors. This type of shading is commonly used to represent + * three-dimensional spheres and cones.
  • + * + *
  • Free-form Gouraud-shaded triangle meshes (type 4) define a + * common construct used by many three-dimensional applications to + * represent complex colored and shaded shapes. Vertices are specified + * in free-form geometry.
  • + * + *
  • Lattice-form Gouraud-shaded triangle meshes (type 5) are based on + * the same geometrical construct as type 4 but with vertices specified + * as a pseudorectangular lattice.
  • + * + *
  • Coons patch meshes (type 6) construct a shading from one or more + * color patches, each bounded by four cubic Bézier curves.
  • + * + *
  • Tensor-product patch meshes (type 7) are similar to type 6 but + * with additional control points in each patch, affording greater + * control over color mapping.
  • + * + * Table 4.28 shows the entries that all shading dictionaries share + * in common; entries specific to particular shading types are + * described in the relevant sections below.

    + */ +public abstract class PDFShader { + + public final static int FUNCTION_SHADING = 1; + public final static int AXIAL_SHADING = 2; + public final static int RADIAL_SHADING = 3; + public final static int FREE_FORM_SHADING = 4; + public final static int LATTICE_SHADING = 5; + public final static int COONS_PATCH_MESH_SHADING = 6; + public final static int TENSOR_PRODUCTS_MESH_SHADING = 7; + + /** The tolerance for reevaluating the shading function again */ + public static float TOLERANCE = 1e-4f; + + /** the type of the shading (1 through 7)*/ + private final int type; + + /** the colorspace */ + private PDFColorSpace colorSpace; + + /** the background color */ + private PDFPaint background; + + /** the bounding box of the pattern */ + private Rectangle2D bbox; + + /** Creates a new instance of PDFShader */ + protected PDFShader(int type) { + this.type = type; + } + + + /** + * Parse a pdf shader into a shader object + */ + public static PDFShader getShader(PDFObject shaderObj, Map resources) + throws IOException + { + // first see if the shader is already cached + PDFShader shader = (PDFShader) shaderObj.getCache(); + if (shader != null) { + return shader; + } + + // read the type (required) + PDFObject typeObj = shaderObj.getDictRef("ShadingType"); + if (typeObj == null) { + throw new PDFParseException("No shader type defined!"); + } + int type = typeObj.getIntValue(); + + // create the shader + switch (type) { + case AXIAL_SHADING: + shader = new ShaderType2(); + break; + + case RADIAL_SHADING: + shader = new ShaderType3(); + break; + + case FUNCTION_SHADING: + case FREE_FORM_SHADING: + case LATTICE_SHADING: + case COONS_PATCH_MESH_SHADING: + case TENSOR_PRODUCTS_MESH_SHADING: + default: + shader = new DummyShader(type); + } + + // read the color space (required) + PDFObject csObj = shaderObj.getDictRef("ColorSpace"); + if (csObj == null) { + throw new PDFParseException("No colorspace defined!"); + } + PDFColorSpace cs = PDFColorSpace.getColorSpace(csObj, resources); + shader.setColorSpace(cs); + + // read the background color (optional) + PDFObject bgObj = shaderObj.getDictRef("Background"); + if (bgObj != null) { + PDFObject[] bgObjs = bgObj.getArray(); + float[] bgArray = new float[bgObjs.length]; + for (int i = 0; i < bgArray.length; i++) { + bgArray[i] = bgObjs[i].getFloatValue(); + } + PDFPaint paint = cs.getPaint(bgArray); + shader.setBackground(paint); + } + + // read the bounding box (optional) + PDFObject bboxObj = shaderObj.getDictRef("BBox"); + if (bboxObj != null) { + PDFObject[] rectObj = bboxObj.getArray(); + float minX = rectObj[0].getFloatValue(); + float minY = rectObj[1].getFloatValue(); + float maxX = rectObj[2].getFloatValue(); + float maxY = rectObj[3].getFloatValue(); + + Rectangle2D bbox = + new Rectangle2D.Float(minX, minY, maxX - minX, maxY - minY); + shader.setBBox(bbox); + } + + // parse the shader-specific attributes + shader.parse(shaderObj); + + // set the cache + shaderObj.setCache(shader); + + return shader; + } + + /** + * Get the type + */ + public int getType() { + return this.type; + } + + /** + * Get the color space + */ + public PDFColorSpace getColorSpace() { + return this.colorSpace; + } + + /** + * Set the color space + */ + protected void setColorSpace(PDFColorSpace colorSpace) { + this.colorSpace = colorSpace; + } + + /** + * Get the background color + */ + public PDFPaint getBackground() { + return this.background; + } + + /** + * Set the background color + */ + protected void setBackground(PDFPaint background) { + this.background = background; + } + + /** + * Get the bounding box + */ + public Rectangle2D getBBox() { + return this.bbox; + } + + /** + * Set the bounding box + */ + protected void setBBox(Rectangle2D bbox) { + this.bbox = bbox; + } + + /** + * Parse the shader-specific data + */ + public abstract void parse(PDFObject shareObj) throws IOException; + + /** + * Returns paint that represents the selected shader + */ + public abstract PDFPaint getPaint(); +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/pattern/PatternType1.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/pattern/PatternType1.java new file mode 100644 index 0000000000..98115ed044 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/pattern/PatternType1.java @@ -0,0 +1,407 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.pattern; + +import java.awt.AlphaComposite; +import java.awt.Graphics2D; +import java.awt.Paint; +import java.awt.PaintContext; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.Shape; +import java.awt.Transparency; +import java.awt.color.ColorSpace; +import java.awt.geom.AffineTransform; +import java.awt.geom.GeneralPath; +import java.awt.geom.NoninvertibleTransformException; +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.Raster; +import java.awt.image.WritableRaster; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFPage; +import com.github.librepdf.pdfrenderer.PDFPaint; +import com.github.librepdf.pdfrenderer.PDFParser; +import com.github.librepdf.pdfrenderer.PDFRenderer; + +/** + * A type 1 (tiling) pattern + */ +public class PatternType1 extends PDFPattern { + /** paint types */ + public static final int PAINT_COLORED = 1; + public static final int PAINT_UNCOLORED = 2; + + /** tiling types */ + public static final int TILE_CONSTANT = 1; + public static final int TILE_NODISTORT = 2; + public static final int TILE_FASTER = 3; + + /** the resources used by the image we will tile */ + private HashMap resources; + + /** the paint type (colored or uncolored) */ + private int paintType; + + /** the tiling type (constant, no distort or faster) */ + private int tilingType; + + /** the bounding box of the tile, in tile space */ + private Rectangle2D bbox; + + /** the horiztonal tile spacing, in tile space */ + private int xStep; + + /** the vertical spacing, in tile space */ + private int yStep; + + /** the stream data */ + private byte[] data; + + /** Creates a new instance of PatternType1 */ + public PatternType1() { + super(1); + } + + /** + * Parse the pattern from the PDFObject + * + * Note the resources passed in are ignored... + */ + @Override + protected void parse(PDFObject patternObj, Map rsrc) throws IOException + { + this.data = patternObj.getStream(); + + this.resources = patternObj.getDictRef("Resources").getDictionary(); + this.paintType = patternObj.getDictRef("PaintType").getIntValue(); + this.tilingType = patternObj.getDictRef("TilingType").getIntValue(); + + PDFObject bboxObj = patternObj.getDictRef("BBox"); + this.bbox= new Rectangle2D.Float(bboxObj.getAt(0).getFloatValue(), + bboxObj.getAt(1).getFloatValue(), + bboxObj.getAt(2).getFloatValue(), + bboxObj.getAt(3).getFloatValue()); + + this.xStep = patternObj.getDictRef("XStep").getIntValue(); + this.yStep = patternObj.getDictRef("YStep").getIntValue(); + } + + /** + * Create a PDFPaint from this pattern and set of components. + * This creates a buffered image of this pattern using + * the given paint, then uses that image to create the correct + * TexturePaint to use in the PDFPaint. + * + * @param basePaint the base paint to use, or null if not needed + */ + @Override + public PDFPaint getPaint(PDFPaint basePaint) { + // create the outline of the pattern in user space by creating + // a box with width xstep and height ystep. Transform that + // box using the pattern's matrix to get the user space + // bounding box + Rectangle2D anchor = new Rectangle2D.Double(getBBox().getMinX(), + getBBox().getMinY(), + getXStep(), + getYStep()); + //anchor = getTransform().createTransformedShape(anchor).getBounds2D(); + + // now create a page bounded by the pattern's user space size + final PDFPage page = new PDFPage(getBBox(), 0); + + // set the base paint if there is one + if (basePaint != null) { + page.addFillPaint(basePaint); + page.addStrokePaint(basePaint); + } + + // undo the page's transform to user space + /* + AffineTransform xform = + new AffineTransform(1, 0, 0, -1, 0, getYStep()); + //new AffineTransform(1, 0, 0, -1, 0, getBBox().getHeight()); + page.addXform(xform); + */ + + // now parse the pattern contents + PDFParser prc = new PDFParser(page, this.data, getResources()); + prc.go(true); + + int width = (int) getBBox().getWidth(); + int height = (int) getBBox().getHeight(); + + // get actual image + Paint paint = new Paint() { + @Override + public PaintContext createContext(ColorModel cm, + Rectangle deviceBounds, + Rectangle2D userBounds, + AffineTransform xform, + RenderingHints hints) + { + ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB); + ColorModel model = new ComponentColorModel(cs, + true, + false, + Transparency.TRANSLUCENT, + DataBuffer.TYPE_BYTE); + + Rectangle2D devBBox = + xform.createTransformedShape(userBounds).getBounds2D(); + + double[] steps = new double[] { getXStep(), getYStep() }; + xform.deltaTransform(steps, 0, steps, 0, 1); + + int width = (int) Math.ceil(devBBox.getWidth()); + int height = (int) Math.ceil(devBBox.getHeight()); + + BufferedImage img = (BufferedImage) page.getImage(width, height, + null, null, + false, true); + + return new Type1PaintContext(model, devBBox, + (float) steps[0], + (float) steps[1], + img.getData()); + } + + @Override + public int getTransparency() { + return Transparency.TRANSLUCENT; + } + }; + + + return new TilingPatternPaint(paint, this); + } + + /** get the associated resources */ + public HashMap getResources() { + return this.resources; + } + + /** get the paint type */ + public int getPaintType() { + return this.paintType; + } + + /** get the tiling type */ + public int getTilingType() { + return this.tilingType; + } + + /** get the bounding box */ + public Rectangle2D getBBox() { + return this.bbox; + } + + /** get the x step */ + public int getXStep() { + return this.xStep; + } + + /** get the y step */ + public int getYStep() { + return this.yStep; + } + + /** + * This class overrides PDFPaint to paint in the pattern coordinate space + */ + static class TilingPatternPaint extends PDFPaint { + /** the pattern to paint */ + private PatternType1 pattern; + + /** Create a tiling pattern paint */ + public TilingPatternPaint(Paint paint, PatternType1 pattern) { + super (paint); + + this.pattern = pattern; + } + + /** + * fill a path with the paint, and record the dirty area. + * @param state the current graphics state + * @param g the graphics into which to draw + * @param s the path to fill + * @param drawn a Rectangle2D into which the dirty area (area drawn) + * will be added. + */ + @Override + public Rectangle2D fill(PDFRenderer state, Graphics2D g, + GeneralPath s) { + // first transform s into device space + AffineTransform at = g.getTransform(); + Shape xformed = s.createTransformedShape(at); + + // push the graphics state so we can restore it + state.push(); + + // set the transform to be the inital transform concatentated + // with the pattern matrix + state.setTransform(state.getInitialTransform()); + state.transform(this.pattern.getTransform()); + + // now figure out where the shape should be + try { + at = state.getTransform().createInverse(); + } catch (NoninvertibleTransformException nte) { + // oh well (?) + } + xformed = at.createTransformedShape(xformed); + + // set the paint and draw the xformed shape + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER)); + g.setPaint(getPaint()); + g.fill(xformed); + + // restore the graphics state + state.pop(); + + // return the area changed + return s.createTransformedShape(g.getTransform()).getBounds2D(); + } + } + + /** + * A simple paint context that uses an existing raster in device + * space to generate pixels + */ + class Type1PaintContext implements PaintContext { + /** the color model */ + private ColorModel colorModel; + + /** the anchor box */ + private Rectangle2D bbox; + + /** the x offset */ + private float xstep; + + /** the y offset */ + private float ystep; + + /** the image data, as a raster in device coordinates */ + private Raster data; + + /** + * Create a paint context + */ + Type1PaintContext(ColorModel colorModel, Rectangle2D bbox, + float xstep, float ystep, Raster data) + { + this.colorModel = colorModel; + this.bbox = bbox; + this.xstep = xstep; + this.ystep = ystep; + this.data = data; + } + + @Override + public void dispose() { + this.colorModel = null; + this.bbox = null; + this.data = null; + } + + @Override + public ColorModel getColorModel() { + return this.colorModel; + } + + @Override + public Raster getRaster(int x, int y, int w, int h) { + ColorSpace cs = getColorModel().getColorSpace(); + + int numComponents = cs.getNumComponents(); + + // all the data, plus alpha channel + int[] imgData = new int[w * h * (numComponents + 1)]; + + // the x and y step, as ints + int useXStep = (int) Math.abs(Math.ceil(this.xstep)); + int useYStep = (int) Math.abs(Math.ceil(this.ystep)); + + // a completely transparent pixel (alpha of 0) + int[] emptyPixel = new int[numComponents + 1]; + int[] usePixel = new int[numComponents + 1]; + + // for each device coordinate + for (int j = 0; j < h; j++) { + for (int i = 0; i < w; i ++) { + // figure out what pixel we are at relative to the image + int xloc = (x + i) - (int) Math.ceil(this.bbox.getX()); + int yloc = (y + j) - (int) Math.ceil(this.bbox.getY()); + + //if useXStep is 0, we would divide through 0 so instead xloc is set to 0 + if(useXStep == 0) { + xloc = 0; + } + else { + xloc %= useXStep; + } + + //if useYStep is 0, we would divide through 0 so instead yloc is set to 0 + if(useYStep == 0) { + yloc = 0; + } + else { + yloc %= useYStep; + } + + if (xloc < 0) { + xloc = useXStep + xloc; + } + if (yloc < 0) { + yloc = useYStep + yloc; + } + + int[] pixel = emptyPixel; + + // check if we are inside the image + if (xloc < this.data.getWidth() && + yloc < this.data.getHeight()) { + pixel = this.data.getPixel(xloc, yloc, usePixel); + } + + int base = (j * w + i) * (numComponents + 1); + for (int c = 0; c < pixel.length; c++) { + imgData[base + c] = pixel[c]; + } + } + } + + WritableRaster raster = + getColorModel().createCompatibleWritableRaster(w, h); + raster.setPixels(0, 0, w, h, imgData); + + Raster child = raster.createTranslatedChild(x, y); + + return child; + } + } +} diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/pattern/PatternType2.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/pattern/PatternType2.java new file mode 100644 index 0000000000..67995a7819 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/pattern/PatternType2.java @@ -0,0 +1,64 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.pattern; + +import java.io.IOException; +import java.util.Map; + +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFPaint; + +/** + * A type 1 (tiling) pattern + */ +public class PatternType2 extends PDFPattern { + + /** the shader */ + private PDFShader shader; + + /** Creates a new instance of PatternType1 */ + public PatternType2() { + super(2); + } + + /** + * Parse the pattern from the PDFObject + * + * Note the resources passed in are ignored... + */ + @Override + protected void parse(PDFObject patternObj, Map rsrc) throws IOException + { + this.shader = PDFShader.getShader(patternObj.getDictRef("Shading"), rsrc); + } + + /** + * Create a PDFPaint from this pattern and set of components. + * This creates a buffered image of this pattern using + * the given paint, then uses that image to create the correct + * TexturePaint to use in the PDFPaint. + * + * @param basePaint the base paint to use, or null if not needed + */ + @Override + public PDFPaint getPaint(PDFPaint basePaint) { + return shader.getPaint(); + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/pattern/ShaderType2.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/pattern/ShaderType2.java new file mode 100644 index 0000000000..41bcf434e7 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/pattern/ShaderType2.java @@ -0,0 +1,396 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.pattern; + +import java.awt.Paint; +import java.awt.PaintContext; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.Transparency; +import java.awt.color.ColorSpace; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.ColorModel; +import java.awt.image.ComponentColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.colorspace.PDFColorSpace; +import com.github.librepdf.pdfrenderer.function.PDFFunction; +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFPaint; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/** + * A shader that performs axial shader based on a function. + */ +public class ShaderType2 extends PDFShader { + /** the start of the axis */ + private Point2D axisStart; + + /** the end of the axis */ + private Point2D axisEnd; + + /** the domain minimum */ + private float minT = 0f; + + /** the domain maximum */ + private float maxT = 1f; + + /** whether to extend the start of the axis */ + private boolean extendStart = false; + + /** whether to extend the end of the axis */ + private boolean extendEnd = false; + + /** functions, as an array of either 1 or n functions */ + private PDFFunction[] functions; + + /** Creates a new instance of ShaderType2 */ + public ShaderType2() { + super(2); + } + + /** + * Parse the shader-specific data + */ + @Override + public void parse(PDFObject shaderObj) throws IOException + { + // read the axis coordinates (required) + PDFObject coordsObj = shaderObj.getDictRef("Coords"); + if (coordsObj == null) { + throw new PDFParseException("No coordinates found!"); + } + PDFObject[] coords = coordsObj.getArray(); + Point2D start = new Point2D.Float(coords[0].getFloatValue(), + coords[1].getFloatValue()); + Point2D end = new Point2D.Float(coords[2].getFloatValue(), + coords[3].getFloatValue()); + setAxisStart(start); + setAxisEnd(end); + + // read the domain (optional) + PDFObject domainObj = shaderObj.getDictRef("Domain"); + if (domainObj != null) { + PDFObject[] domain = domainObj.getArray(); + setMinT(domain[0].getFloatValue()); + setMaxT(domain[1].getFloatValue()); + } + + // read the functions (required) + PDFObject functionObj = shaderObj.getDictRef("Function"); + if (functionObj == null) { + throw new PDFParseException("No function defined for shader!"); + } + PDFObject[] functionArray = functionObj.getArray(); + PDFFunction[] functions = new PDFFunction[functionArray.length]; + for (int i = 0; i < functions.length; i++) { + functions[i] = PDFFunction.getFunction(functionArray[i]); + } + setFunctions(functions); + + // read the extend array (optional) + PDFObject extendObj = shaderObj.getDictRef("Extend"); + if (extendObj != null) { + PDFObject[] extendArray = extendObj.getArray(); + setExtendStart(extendArray[0].getBooleanValue()); + setExtendEnd(extendArray[1].getBooleanValue()); + } + + } + + /** + * Create a paint that paints this pattern + */ + @Override + public PDFPaint getPaint() { + return PDFPaint.getPaint(new Type2Paint()); + } + + /** + * Get the start of the axis + */ + public Point2D getAxisStart() { + return this.axisStart; + } + + /** + * Set the start of the axis + */ + protected void setAxisStart(Point2D axisStart) { + this.axisStart = axisStart; + } + + /** + * Get the end of the axis + */ + public Point2D getAxisEnd() { + return this.axisEnd; + } + + /** + * Set the start of the axis + */ + protected void setAxisEnd(Point2D axisEnd) { + this.axisEnd = axisEnd; + } + + /** + * Get the domain minimum + */ + public float getMinT() { + return this.minT; + } + + /** + * Set the domain minimum + */ + protected void setMinT(float minT) { + this.minT = minT; + } + + /** + * Get the domain maximum + */ + public float getMaxT() { + return this.maxT; + } + + /** + * Set the domain maximum + */ + protected void setMaxT(float maxT) { + this.maxT = maxT; + } + + /** + * Get whether to extend the start of the axis + */ + public boolean getExtendStart() { + return this.extendStart; + } + + /** + * Set whether to extend the start of the axis + */ + protected void setExtendStart(boolean extendStart) { + this.extendStart = extendStart; + } + + /** + * Get whether to extend the end of the axis + */ + public boolean getExtendEnd() { + return this.extendEnd; + } + + /** + * Set whether to extend the end of the axis + */ + protected void setExtendEnd(boolean extendEnd) { + this.extendEnd = extendEnd; + } + + /** + * Get the functions associated with this shader + */ + public PDFFunction[] getFunctions() { + return this.functions; + } + + /** + * Set the functions associated with this shader + */ + protected void setFunctions(PDFFunction[] functions) { + this.functions = functions; + } + + /** + * A subclass of paint that uses this shader to generate a paint + */ + class Type2Paint implements Paint { + public Type2Paint() { + } + + /** create a paint context */ + @Override + public PaintContext createContext(ColorModel cm, + Rectangle deviceBounds, + Rectangle2D userBounds, + AffineTransform xform, + RenderingHints hints) + { + ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB); + ColorModel model = new ComponentColorModel(cs, + true, + false, + Transparency.TRANSLUCENT, + DataBuffer.TYPE_BYTE); + + Point2D devStart = xform.transform(getAxisStart(), null); + Point2D devEnd = xform.transform(getAxisEnd(), null); + + return new Type2PaintContext(model, devStart, devEnd); + } + + @Override + public int getTransparency() { + return Transparency.TRANSLUCENT; + } + } + + /** + * A simple paint context that uses an existing raster in device + * space to generate pixels + */ + class Type2PaintContext implements PaintContext { + /** the color model */ + private ColorModel colorModel; + + /** the start of the axis */ + private Point2D start; + + /** the end of the axis */ + private Point2D end; + + + private float dt1t0; + private double dx1x0, dy1y0, sqdx1x0psqdy1y0; + + /** + * Create a paint context + */ + Type2PaintContext(ColorModel colorModel, Point2D start, Point2D end) { + this.colorModel = colorModel; + this.start = start; + this.end = end; + + //pre calculate some often used values + dt1t0 = getMaxT() - getMinT(); + dx1x0 = end.getX() - start.getX(); + dy1y0 = end.getY() - start.getY(); + sqdx1x0psqdy1y0 = dx1x0*dx1x0 + dy1y0*dy1y0; + } + + @Override + public void dispose() { + this.colorModel = null; + } + + @Override + public ColorModel getColorModel() { + return this.colorModel; + } + + @Override + public Raster getRaster(int x, int y, int w, int h) { + ColorSpace cs = getColorModel().getColorSpace(); + PDFColorSpace shadeCSpace = getColorSpace(); + + + PDFFunction functions[] = getFunctions(); + int numComponents = cs.getNumComponents(); + + float x0 = (float) this.start.getX(); + float y0 = (float) this.start.getY(); + + float[] inputs = new float[1]; + float[] outputs = new float[shadeCSpace.getNumComponents()]; + float[] outputRBG = new float[numComponents]; + + // all the data, plus alpha channel + int[] data = new int[w * h * (numComponents + 1)]; + + // for each device coordinate + for (int j = 0; j < h; j++) { + for (int i = 0; i < w; i += 1) { + boolean render = true; + // find t for that user coordinate + float xp = getXPrime(i + x, j + y, x0, y0); + float t = 0; + if (xp >= 0 && xp <= 1) t = getMinT() + (dt1t0 * xp); + else if (xp < 0 && extendStart) t = getMinT(); + else if (xp > 1 && extendEnd) t = getMaxT(); + else render = false; + + if (render) { + // calculate the pixel values at t + inputs[0] = t; + if (functions.length == 1) { + functions[0].calculate(inputs, 0, outputs, 0); + } else { + for (int c = 0; c < functions.length; c++) { + functions[c].calculate(inputs, 0, outputs, c); + } + } + if (functions[0].getNumOutputs() != numComponents) { + //CMYK + outputRBG = shadeCSpace.getColorSpace().toRGB(outputs); + } + else outputRBG = outputs; + + int base = (j * w + i) * (numComponents + 1); + for (int c = 0; c < numComponents; c++) { + data[base + c] = (int) (outputRBG[c] * 255); + } + data[base + numComponents] = 255; + } + } + } + + WritableRaster raster = + getColorModel().createCompatibleWritableRaster(w, h); + raster.setPixels(0, 0, w, h, data); + + Raster child = raster.createTranslatedChild(x, y); + return child; + } + + /** + * x' = (x1 - x0) * (x - x0) + (y1 - y0) * (y - y0) + * ------------------------------------------- + * (x1 - x0)^2 + (y1 - y0)^2 + */ + private float getXPrime(float x, float y, float x0, float y0) { + + double tp = ((dx1x0* (x - x0)) + (dy1y0 * (y - y0))) / sqdx1x0psqdy1y0; + + return (float) tp; + } + + /** + * t = t0 + (t1 - t0) x x' + */ + private float getT(float xp) { + + if (xp < 0) { + return getMinT(); + } else if (xp > 1) { + return getMaxT(); + } else { + return getMinT() + (dt1t0 * xp); + } + } + } +} \ No newline at end of file diff --git a/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/pattern/ShaderType3.java b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/pattern/ShaderType3.java new file mode 100644 index 0000000000..a4a233722a --- /dev/null +++ b/openpdf-pdfrenderer/src/main/java/com/github/librepdf/pdfrenderer/pattern/ShaderType3.java @@ -0,0 +1,389 @@ +/* + * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, + * Santa Clara, California 95054, U.S.A. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package com.github.librepdf.pdfrenderer.pattern; + +import java.awt.Paint; +import java.awt.PaintContext; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.Transparency; +import java.awt.color.ColorSpace; +import java.awt.geom.AffineTransform; +import java.awt.geom.NoninvertibleTransformException; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.ColorModel; +import java.awt.image.ComponentColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; +import java.io.IOException; + +import com.github.librepdf.pdfrenderer.colorspace.PDFColorSpace; +import com.github.librepdf.pdfrenderer.function.PDFFunction; +import com.github.librepdf.pdfrenderer.BaseWatchable; +import com.github.librepdf.pdfrenderer.PDFObject; +import com.github.librepdf.pdfrenderer.PDFPaint; +import com.github.librepdf.pdfrenderer.PDFParseException; + +/** + * A shader that performs radial shader based on a function. + */ +public class ShaderType3 extends PDFShader { + /** the center of the first circle */ + private Point2D center1; + + /** the center of the second circle */ + private Point2D center2; + + /** the radius of the first circle */ + private float radius1; + + /** the radius of the second circle */ + private float radius2; + + /** the domain minimum */ + private float minT = 0f; + + /** the domain maximum */ + private float maxT = 1f; + + /** whether to extend the start of the axis */ + private boolean extendStart = false; + + /** whether to extend the end of the axis */ + private boolean extendEnd = false; + + /** functions, as an array of either 1 or n functions */ + private PDFFunction[] functions; + + /** Creates a new instance of ShaderType2 */ + public ShaderType3() { + super(3); + } + + /** + * Parse the shader-specific data + */ + @Override + public void parse(PDFObject shaderObj) throws IOException + { + // read the axis coordinates (required) + PDFObject coordsObj = shaderObj.getDictRef("Coords"); + if (coordsObj == null) { + throw new PDFParseException("No coordinates found!"); + } + PDFObject[] coords = coordsObj.getArray(); + center1 = new Point2D.Float(coords[0].getFloatValue(), + coords[1].getFloatValue()); + center2 = new Point2D.Float(coords[3].getFloatValue(), + coords[4].getFloatValue()); + radius1 = coords[2].getFloatValue(); + radius2 = coords[5].getFloatValue(); + + // read the domain (optional) + PDFObject domainObj = shaderObj.getDictRef("Domain"); + if (domainObj != null) { + PDFObject[] domain = domainObj.getArray(); + setMinT(domain[0].getFloatValue()); + setMaxT(domain[1].getFloatValue()); + } + + // read the functions (required) + PDFObject functionObj = shaderObj.getDictRef("Function"); + if (functionObj == null) { + throw new PDFParseException("No function defined for shader!"); + } + PDFObject[] functionArray = functionObj.getArray(); + PDFFunction[] functions = new PDFFunction[functionArray.length]; + for (int i = 0; i < functions.length; i++) { + functions[i] = PDFFunction.getFunction(functionArray[i]); + } + setFunctions(functions); + + // read the extend array (optional) + PDFObject extendObj = shaderObj.getDictRef("Extend"); + if (extendObj != null) { + PDFObject[] extendArray = extendObj.getArray(); + setExtendStart(extendArray[0].getBooleanValue()); + setExtendEnd(extendArray[1].getBooleanValue()); + } + } + + /** + * Create a paint that paints this pattern + */ + @Override + public PDFPaint getPaint() { + return PDFPaint.getPaint(new Type3Paint()); + } + + + /** + * Get the domain minimum + */ + public float getMinT() { + return this.minT; + } + + /** + * Set the domain minimum + */ + protected void setMinT(float minT) { + this.minT = minT; + } + + /** + * Get the domain maximum + */ + public float getMaxT() { + return this.maxT; + } + + /** + * Set the domain maximum + */ + protected void setMaxT(float maxT) { + this.maxT = maxT; + } + + /** + * Get whether to extend the start of the axis + */ + public boolean getExtendStart() { + return this.extendStart; + } + + /** + * Set whether to extend the start of the axis + */ + protected void setExtendStart(boolean extendStart) { + this.extendStart = extendStart; + } + + /** + * Get whether to extend the end of the axis + */ + public boolean getExtendEnd() { + return this.extendEnd; + } + + /** + * Set whether to extend the end of the axis + */ + protected void setExtendEnd(boolean extendEnd) { + this.extendEnd = extendEnd; + } + + /** + * Get the functions associated with this shader + */ + public PDFFunction[] getFunctions() { + return this.functions; + } + + /** + * Set the functions associated with this shader + */ + protected void setFunctions(PDFFunction[] functions) { + this.functions = functions; + } + + /** + * A subclass of paint that uses this shader to generate a paint + */ + class Type3Paint implements Paint { + public Type3Paint() { + } + + /** create a paint context */ + @Override + public PaintContext createContext(ColorModel cm, + Rectangle deviceBounds, + Rectangle2D userBounds, + AffineTransform xform, + RenderingHints hints) + { + ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB); + ColorModel model = new ComponentColorModel(cs, + true, + false, + Transparency.TRANSLUCENT, + DataBuffer.TYPE_BYTE); + + return new Type3PaintContext(model, xform); + } + + @Override + public int getTransparency() { + return Transparency.TRANSLUCENT; + } + } + + /** + * A simple paint context that uses an existing raster in device + * space to generate pixels + */ + class Type3PaintContext implements PaintContext { + /** the color model */ + private ColorModel colorModel; + + /** the transformation */ + private AffineTransform invXform; + + private final double dx1x0, dy1y0, dr1r0, sqr0, denom; + + /** + * Create a paint context + */ + Type3PaintContext(ColorModel colorModel, AffineTransform xform) { + this.colorModel = colorModel; + + //Precalculate some often needed values; + dx1x0 = center2.getX() - center1.getX(); + dy1y0 = center2.getY() - center1.getY(); + dr1r0 = radius2 - radius1; + sqr0 = radius1*radius1; + denom = dx1x0*dx1x0 + dy1y0*dy1y0 - dr1r0*dr1r0; + + try { + this.invXform = xform.createInverse(); + } + catch (NoninvertibleTransformException e) { + BaseWatchable.getErrorHandler().publishException(e); + } + } + + @Override + public void dispose() { + this.colorModel = null; + } + + @Override + public ColorModel getColorModel() { + return this.colorModel; + } + + @Override + public Raster getRaster(int x, int y, int w, int h) { + ColorSpace cs = getColorModel().getColorSpace(); + PDFColorSpace shadeCSpace = getColorSpace(); + + PDFFunction functions[] = getFunctions(); + + int numComponents = cs.getNumComponents(); + + float[] c1 = new float[2]; + + float[] inputs = new float[1]; + float[] outputs = new float[shadeCSpace.getNumComponents()]; + float[] outputRBG = new float[numComponents]; + + // all the data, plus alpha channel + int[] data = new int[w * h * (numComponents + 1)]; + float lastInput = Float.POSITIVE_INFINITY; + final float tol = TOLERANCE * (getMaxT() - getMinT()); + + final int advance = 1; + // for each device coordinate + for (int j = 0; j < h; j++) { + for (int i = 0; i < w; i += advance) { + //Get point in user space + invXform.transform(new float[]{x + i, y + j}, 0, c1, 0, 1); + boolean render = true; + float[] s = calculateInputValues(c1[0], c1[1]); + //s[0] <= s[1] holds + //if (s[0] >= 0 && s[1] <= 1) s[1] = s[1]; + if (s[1] >= 0 && s[1] <= 1) s[1] = s[1]; + else if (extendEnd == true && s[1] >= 0 && radius1 + s[1]*dr1r0 >= 0) { + s[1] = s[1]; + } + else if (s[0] >= 0 && s[0] <= 1) s[1] = s[0]; + else if (extendStart == true && s[1] <= 0 && radius1 + s[1]*dr1r0 >= 0) { + s[1] = s[1]; + } + else if (extendStart == true && s[0] <= 1 && radius1 + s[0]*dr1r0 >= 0) { + s[1] = s[0]; + } + else render = false; + + if (render) { + float t = (getMinT() + s[1]*(getMaxT() - getMinT())); + // calculate the pixel values at t + inputs[0] = t; + if (Math.abs(lastInput - t) > tol) { + + if (functions.length == 1) { + functions[0].calculate(inputs, 0, outputs, 0); + } else { + for (int c = 0; c < functions.length; c++) { + functions[c].calculate(inputs, 0, outputs, c); + } + } + + if (!shadeCSpace.getColorSpace().isCS_sRGB()) { + //Can be quite slow + outputRBG = shadeCSpace.getColorSpace().toRGB(outputs); + } + else outputRBG = outputs; + + lastInput = t; + } + int base = (j * w + i) * (numComponents + 1); + for (int c = 0; c < numComponents; c++) { + data[base + c] = (int) (outputRBG[c] * 255); + } + data[base + numComponents] = 255; + } + } + } + + WritableRaster raster = + getColorModel().createCompatibleWritableRaster(w, h); + raster.setPixels(0, 0, w, h, data); + + Raster child = raster.createTranslatedChild(x, y); + return child; + } + + /** + * From Adobe Technical Note #5600: + * + * Given a geometric coordinate position (x, y) in or along the gradient gradient fill, + * the corresponding value of s can be determined by solving the quadratic + * constraint equation: + * + * [x - xc(s)]2 + [y - yc(s)]2 = [r(s)]2 + * + * The following code calculates the 2 possible values of s. + * + * @return Two possible values of s with s[0] <= s[1] + */ + private float[] calculateInputValues(float x, float y) { + double p = -(x - center1.getX())*dx1x0 -(y - center1.getY())*dy1y0 - radius1*dr1r0; + double q = (Math.pow(x - center1.getX(), 2) + Math.pow(y - center1.getY(), 2) - sqr0); + double root = Math.sqrt(p*p - denom*q); + float root1 = (float) ((-p + root)/denom); + float root2 = (float) ((-p - root)/denom); + if (denom < 0) return new float[]{root1, root2}; + else return new float[]{root2, root1}; + } + } +} diff --git a/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/BaseFonts.properties b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/BaseFonts.properties new file mode 100644 index 0000000000..4659433386 --- /dev/null +++ b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/BaseFonts.properties @@ -0,0 +1,57 @@ +# Sample ResourceBundle properties file + +Courier.file=n022003l.pfb + +Courier.length=96263 + +Courier-Bold.file=n022004l.pfb + +Courier-Bold.length=120373 + +Courier-BoldOblique.file=n022024l.pfb + +Courier-BoldOblique.length=114228 + +Courier-Oblique.file=n022023l.pfb + +Courier-Oblique.length=101133 + +Helvetica.file=n019003l.pfb + +Helvetica.length=68590 + +Helvetica-Bold.file=n019004l.pfb + +Helvetica-Bold.length=72400 + +Helvetica-BoldOblique.file=n019024l.pfb + +Helvetica-BoldOblique.length=73879 + +Helvetica-Oblique.file=n019023l.pfb + +Helvetica-Oblique.length=71719 + +Times-Roman.file=n021003l.pfb + +Times-Roman.length=113206 + +Times-Bold.file=n021004l.pfb + +Times-Bold.length=108822 + +Times-BoldItalic.file=n021024l.pfb + +Times-BoldItalic.length=96211 + +Times-Italic.file=n021023l.pfb + +Times-Italic.length=108217 + +Symbol.file=s050000l.pfb + +Symbol.length=32213 + +ZapfDingbats.file=d050000l.pfb + +ZapfDingbats.length=45955 diff --git a/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/d050000l.pfb b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/d050000l.pfb new file mode 100644 index 0000000000..4a3c386d29 Binary files /dev/null and b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/d050000l.pfb differ diff --git a/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n019003l.pfb b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n019003l.pfb new file mode 100644 index 0000000000..82b6ad8614 Binary files /dev/null and b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n019003l.pfb differ diff --git a/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n019004l.pfb b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n019004l.pfb new file mode 100644 index 0000000000..94a302084f Binary files /dev/null and b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n019004l.pfb differ diff --git a/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n019023l.pfb b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n019023l.pfb new file mode 100644 index 0000000000..b124a58df3 Binary files /dev/null and b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n019023l.pfb differ diff --git a/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n019024l.pfb b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n019024l.pfb new file mode 100644 index 0000000000..1a9d6f5b10 Binary files /dev/null and b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n019024l.pfb differ diff --git a/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n021003l.pfb b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n021003l.pfb new file mode 100644 index 0000000000..08261c6243 Binary files /dev/null and b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n021003l.pfb differ diff --git a/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n021004l.pfb b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n021004l.pfb new file mode 100644 index 0000000000..2b59a6714d Binary files /dev/null and b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n021004l.pfb differ diff --git a/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n021023l.pfb b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n021023l.pfb new file mode 100644 index 0000000000..8e4db51a4b Binary files /dev/null and b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n021023l.pfb differ diff --git a/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n021024l.pfb b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n021024l.pfb new file mode 100644 index 0000000000..67bc587164 Binary files /dev/null and b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n021024l.pfb differ diff --git a/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n022003l.pfb b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n022003l.pfb new file mode 100644 index 0000000000..ecd871af7e Binary files /dev/null and b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n022003l.pfb differ diff --git a/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n022004l.pfb b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n022004l.pfb new file mode 100644 index 0000000000..da2d22c5e4 Binary files /dev/null and b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n022004l.pfb differ diff --git a/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n022023l.pfb b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n022023l.pfb new file mode 100644 index 0000000000..b0edaf25e0 Binary files /dev/null and b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n022023l.pfb differ diff --git a/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n022024l.pfb b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n022024l.pfb new file mode 100644 index 0000000000..f32adb41c0 Binary files /dev/null and b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/n022024l.pfb differ diff --git a/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/s050000l.pfb b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/s050000l.pfb new file mode 100644 index 0000000000..3c1672306d Binary files /dev/null and b/openpdf-pdfrenderer/src/main/resources/com/github/librepdf/pdfrenderer/font/res/s050000l.pfb differ diff --git a/openpdf-pdfrenderer/src/test/java/PdfRendererToImageTest.java b/openpdf-pdfrenderer/src/test/java/PdfRendererToImageTest.java new file mode 100644 index 0000000000..1e30038bf2 --- /dev/null +++ b/openpdf-pdfrenderer/src/test/java/PdfRendererToImageTest.java @@ -0,0 +1,145 @@ +/* + * Copyright 2024 OpenPDF + * + * The contents of this file are subject to the Mozilla Public License Version 1.1 + * (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the License. + * + * The Original Code is 'iText, a free JAVA-PDF library'. + * + * The Initial Developer of the Original Code is Bruno Lowagie. Portions created by + * the Initial Developer are Copyright (C) 1999, 2000, 2001, 2002 by Bruno Lowagie. + * All Rights Reserved. + * Co-Developer of the code is Paulo Soares. Portions created by the Co-Developer + * are Copyright (C) 2000, 2001, 2002 by Paulo Soares. All Rights Reserved. + * + * Contributor(s): all the names of the contributors are added in the source code + * where applicable. + * + * Alternatively, the contents of this file may be used under the terms of the + * LGPL license (the "GNU LIBRARY GENERAL PUBLIC LICENSE"), in which case the + * provisions of LGPL are applicable instead of those above. If you wish to + * allow use of your version of this file only under the terms of the LGPL + * License and not to allow others to use your version of this file under + * the MPL, indicate your decision by deleting the provisions above and + * replace them with the notice and other provisions required by the LGPL. + * If you do not delete the provisions above, a recipient may use your version + * of this file under either the MPL or the GNU LIBRARY GENERAL PUBLIC LICENSE. + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the MPL as stated above or under the terms of the GNU + * Library General Public License as published by the Free Software Foundation; + * either version 2 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Library general Public License for more + * details. + * + * If you didn't download this code from the following link, you should check if + * you aren't using an obsolete version: + * https://github.com/LibrePDF/OpenPDF + */ + + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import com.github.librepdf.pdfrenderer.PDFFile; +import com.github.librepdf.pdfrenderer.PDFPage; +import com.github.librepdf.pdfrenderer.PDFRenderer; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.*; + +public class PdfRendererToImageTest { + + private byte[][] pdfBytesArray; + + @BeforeEach + public void setUp() throws Exception { + // Array of PDF file names + String[] pdfFiles = {"invoice-1.pdf", "pdfsmartcopy_bec_image.pdf"}; + pdfBytesArray = new byte[pdfFiles.length][]; + + // Load each PDF file from the resources directory and convert it to bytes + for (int i = 0; i < pdfFiles.length; i++) { + try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(pdfFiles[i])) { + assertNotNull(inputStream, "PDF file not found in resources: " + pdfFiles[i]); + pdfBytesArray[i] = readPdfToByteArray(inputStream); + } + } + } + + private byte[] readPdfToByteArray(InputStream inputStream) throws IOException { + // Read the entire InputStream into a ByteArrayOutputStream, then convert to byte[] + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length; + while ((length = inputStream.read(buffer)) != -1) { + baos.write(buffer, 0, length); + } + return baos.toByteArray(); + } + + @Test + public void testPDFRenderToImage() throws IOException { + String[] pdfFiles = {"invoice-1.pdf", "pdfsmartcopy_bec_image.pdf"}; + + for (int i = 0; i < pdfBytesArray.length; i++) { + byte[] pdfBytes = pdfBytesArray[i]; + + // Step 1: Load the PDF using PDFRenderer + PDFFile pdfFile = new PDFFile(ByteBuffer.wrap(pdfBytes)); + + // Ensure there is at least one page in the PDF + int numPages = pdfFile.getNumPages(); + assertTrue(numPages > 0, "PDF should contain at least one page"); + + // Use the correct page index (PDFRenderer typically uses 1-based indexing) + PDFPage page = pdfFile.getPage(1, true); // Fetch the first page + + // Step 2: Setup the dimensions for the output image + int width = (int) page.getBBox().getWidth(); + int height = (int) page.getBBox().getHeight(); + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics2D g2d = image.createGraphics(); + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + + // Step 3: Render the page to the image + Rectangle rect = new Rectangle(0, 0, width, height); + PDFRenderer renderer = new PDFRenderer(page, g2d, rect, null, Color.WHITE); + + renderer.run(); + + // Step 4: Save the image to a file (for verification) + File outputfile = new File("pdf-to-image-test-output-" + pdfFiles[i] + ".png"); + ImageIO.write(image, "png", outputfile); + + // Step 5: Verify that the image was saved correctly + assertTrue(outputfile.exists(), "Output image file should exist for " + pdfFiles[i]); + assertTrue(outputfile.length() > 0, "Output image file should not be empty for " + pdfFiles[i]); + + // Step 6: Load the image back and check its properties + BufferedImage loadedImage = ImageIO.read(outputfile); + assertNotNull(loadedImage, "Loaded image should not be null for " + pdfFiles[i]); + assertEquals(width, loadedImage.getWidth(), "Image width should match the PDF page width for " + pdfFiles[i]); + assertEquals(height, loadedImage.getHeight(), "Image height should match the PDF page height for " + pdfFiles[i]); + + // Cleanup graphics object + g2d.dispose(); + } + } +} diff --git a/openpdf-pdfrenderer/src/test/resources/invoice-1.pdf b/openpdf-pdfrenderer/src/test/resources/invoice-1.pdf new file mode 100644 index 0000000000..9074e23b45 Binary files /dev/null and b/openpdf-pdfrenderer/src/test/resources/invoice-1.pdf differ diff --git a/openpdf-pdfrenderer/src/test/resources/pdfsmartcopy_bec_image.pdf b/openpdf-pdfrenderer/src/test/resources/pdfsmartcopy_bec_image.pdf new file mode 100644 index 0000000000..63d56fd199 Binary files /dev/null and b/openpdf-pdfrenderer/src/test/resources/pdfsmartcopy_bec_image.pdf differ diff --git a/openpdf/src/main/java/com/lowagie/text/pdf/parser/PdfTextExtractor.java b/openpdf/src/main/java/com/lowagie/text/pdf/parser/PdfTextExtractor.java index 756ddc0644..cd8c0e9097 100644 --- a/openpdf/src/main/java/com/lowagie/text/pdf/parser/PdfTextExtractor.java +++ b/openpdf/src/main/java/com/lowagie/text/pdf/parser/PdfTextExtractor.java @@ -77,12 +77,12 @@ public class PdfTextExtractor { /** * The PdfReader that holds the PDF file. */ - private final PdfReader reader; + protected final PdfReader reader; /** * The {@link TextAssembler} that will receive render notifications and provide resultant text */ - private final TextAssembler renderListener; + protected final TextAssembler renderListener; /** * Creates a new Text Extractor object, using a {@link TextAssembler} as the render listener @@ -122,7 +122,7 @@ public PdfTextExtractor(PdfReader reader, TextAssembler renderListener) { * @return a byte array with the effective content stream of a page * @throws IOException */ - private byte[] getContentBytesForPage(int pageNum) throws IOException { + protected byte[] getContentBytesForPage(int pageNum) throws IOException { try (RandomAccessFileOrArray ignored = reader.getSafeFile()) { PdfDictionary pageDictionary = reader.getPageN(pageNum); PdfObject contentObject = pageDictionary.get(PdfName.CONTENTS); diff --git a/openpdf/src/main/java/com/lowagie/text/pdf/parser/RegionPdfContentStreamHandler.java b/openpdf/src/main/java/com/lowagie/text/pdf/parser/RegionPdfContentStreamHandler.java new file mode 100644 index 0000000000..9d4ce4e94d --- /dev/null +++ b/openpdf/src/main/java/com/lowagie/text/pdf/parser/RegionPdfContentStreamHandler.java @@ -0,0 +1,127 @@ +/* + * Copyright 2024 OpenPDF + * + * The contents of this file are subject to the Mozilla Public License Version 1.1 + * (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the License. + * + * The Original Code is 'OpenPDF'. + * + * The Initial Developer of the Original Code is Bruno Lowagie. Portions created by + * the Initial Developer are Copyright (C) 1999-2008 by Bruno Lowagie. + * All Rights Reserved. + * Co-Developer of the code is Paulo Soares. Portions created by the Co-Developer + * are Copyright (C) 2000-2008 by Paulo Soares. All Rights Reserved. + * + * Contributor(s): all the names of the contributors are added in the source code + * where applicable. + * + * Alternatively, the contents of this file may be used under the terms of the + * LGPL license (the "GNU LIBRARY GENERAL PUBLIC LICENSE"), in which case the + * provisions of LGPL are applicable instead of those above. If you wish to + * allow use of your version of this file only under the terms of the LGPL + * License and not to allow others to use your version of this file under + * the MPL, indicate your decision by deleting the provisions above and + * replace them with the notice and other provisions required by the LGPL. + * If you do not delete the provisions above, a recipient may use your version + * of this file under either the MPL or the GNU LIBRARY GENERAL PUBLIC LICENSE. + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the MPL as stated above or under the terms of the GNU + * Library General Public License as published by the Free Software Foundation; + * either version 2 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Library general Public License for more + * details. + * + * If you didn't download this code from the following link, you should check if + * you aren't using an obsolete version: + * https://github.com/LibrePDF/OpenPDF + */ +package com.lowagie.text.pdf.parser; + +import com.lowagie.text.pdf.PdfArray; +import com.lowagie.text.pdf.PdfDictionary; +import com.lowagie.text.pdf.PdfLiteral; +import com.lowagie.text.pdf.PdfNumber; +import com.lowagie.text.pdf.PdfObject; +import com.lowagie.text.pdf.PdfString; + +import java.awt.Rectangle; +import java.util.List; + +/** + * Stream handler for reading a specific part of a PDF document. + * + * @since 2.1.4 + */ +public class RegionPdfContentStreamHandler extends PdfContentStreamHandler { + private Rectangle boundingBox; + + public RegionPdfContentStreamHandler(TextAssembler renderListener) { + super(renderListener); + } + + /** + * Sets the bounding box to filter text extraction. If boundingBox is null, + * no filtering will be applied. + * 0,0 is in the bottom left part of the page. + */ + public void setBoundingBox(float x, float y, float width, float height) { + this.boundingBox = new Rectangle((int) x, (int) y, (int) width, (int) height); + } + + /** + * Overrides the invokeOperator method to include bounding box checking. + */ + @Override + public void invokeOperator(PdfLiteral operator, List operands, PdfDictionary resources) { + String op = operator.toString(); + + if ("Tj".equals(op)) { + // Single text string to show + PdfString text = (PdfString) operands.get(0); + float[] textPosition = getCurrentTextPosition(); + if (boundingBox == null || isWithinBoundingBox(textPosition[0], textPosition[1])) { + displayPdfString(text); + } + } else if ("TJ".equals(op)) { + // Array of text strings and positioning adjustments + PdfArray array = (PdfArray) operands.get(0); + float[] textPosition = getCurrentTextPosition(); + if (boundingBox == null || isWithinBoundingBox(textPosition[0], textPosition[1])) { + for (PdfObject obj : array.getElements()) { + if (obj instanceof PdfString) { + displayPdfString((PdfString) obj); + } else if (obj instanceof PdfNumber) { + applyTextAdjust(((PdfNumber) obj).floatValue()); + } + } + } + } else { + // Handle other operators as before + super.invokeOperator(operator, operands, resources); + } + } + + /** + * Checks if a text element is within the bounding box. + */ + private boolean isWithinBoundingBox(float textX, float textY) { + return boundingBox != null && boundingBox.contains(textX, textY); + } + + /** + * Method to get the current text position based on the text matrix. + */ + private float[] getCurrentTextPosition() { + Matrix textMatrix = getCurrentTextMatrix(); + return new float[]{textMatrix.get(Matrix.I31), textMatrix.get(Matrix.I32)}; + } +} \ No newline at end of file diff --git a/openpdf/src/main/java/com/lowagie/text/pdf/parser/RegionPdfTextExtractor.java b/openpdf/src/main/java/com/lowagie/text/pdf/parser/RegionPdfTextExtractor.java new file mode 100644 index 0000000000..e0f0ac437d --- /dev/null +++ b/openpdf/src/main/java/com/lowagie/text/pdf/parser/RegionPdfTextExtractor.java @@ -0,0 +1,145 @@ +/* + * Copyright 2024 OpenPDF + * + * The contents of this file are subject to the Mozilla Public License Version 1.1 + * (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the License. + * + * The Original Code is 'OpenPDF'. + * + * The Initial Developer of the Original Code is Bruno Lowagie. Portions created by + * the Initial Developer are Copyright (C) 1999-2008 by Bruno Lowagie. + * All Rights Reserved. + * Co-Developer of the code is Paulo Soares. Portions created by the Co-Developer + * are Copyright (C) 2000-2008 by Paulo Soares. All Rights Reserved. + * + * Contributor(s): all the names of the contributors are added in the source code + * where applicable. + * + * Alternatively, the contents of this file may be used under the terms of the + * LGPL license (the "GNU LIBRARY GENERAL PUBLIC LICENSE"), in which case the + * provisions of LGPL are applicable instead of those above. If you wish to + * allow use of your version of this file only under the terms of the LGPL + * License and not to allow others to use your version of this file under + * the MPL, indicate your decision by deleting the provisions above and + * replace them with the notice and other provisions required by the LGPL. + * If you do not delete the provisions above, a recipient may use your version + * of this file under either the MPL or the GNU LIBRARY GENERAL PUBLIC LICENSE. + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the MPL as stated above or under the terms of the GNU + * Library General Public License as published by the Free Software Foundation; + * either version 2 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Library general Public License for more + * details. + * + * If you didn't download this code from the following link, you should check if + * you aren't using an obsolete version: + * https://github.com/LibrePDF/OpenPDF + */ + +package com.lowagie.text.pdf.parser; + +import com.lowagie.text.pdf.*; + +import java.io.IOException; + +/** + * Extracts text from a specific region of a PDF file. + * + * @since 2.0.4 + */ +public class RegionPdfTextExtractor extends PdfTextExtractor { + + public RegionPdfTextExtractor(PdfReader reader) { + super(reader); + } + + public RegionPdfTextExtractor(PdfReader reader, TextAssembler renderListener) { + super(reader, renderListener); + } + + /** + * Extracts text from a specific area of a page of a PDF document. + * + * 0,0 is the bottom left part of the page. + */ + public String getTextFromPageArea(int page, float x, float y, float width, float height) throws IOException { + PdfDictionary pageDict = reader.getPageN(page); + if (pageDict == null) { + return ""; + } + PdfDictionary resources = pageDict.getAsDict(PdfName.RESOURCES); + + renderListener.reset(); + renderListener.setPage(page); + RegionPdfContentStreamHandler handler = new RegionPdfContentStreamHandler(renderListener); + handler.setBoundingBox(x, y, width, height); + processContent(getContentBytesForPage(page), resources, handler); + return handler.getResultantText(); + } + + /** + * Gets the height of the specified page. + */ + public float getPageHeight(int page) throws IOException { + PdfDictionary pageDict = reader.getPageN(page); + if (pageDict == null) { + throw new IOException("Page dictionary not found for page: " + page); + } + PdfArray mediaBox = pageDict.getAsArray(PdfName.MEDIABOX); + if (mediaBox == null || mediaBox.size() < 4) { + throw new IOException("MediaBox not found or invalid for page: " + page); + } + PdfNumber lowerLeftY = mediaBox.getAsNumber(1); + PdfNumber upperRightY = mediaBox.getAsNumber(3); + return upperRightY.floatValue() - lowerLeftY.floatValue(); + } + + /** + * Gets the width of the specified page. + */ + public float getPageWidth(int page) throws IOException { + PdfDictionary pageDict = reader.getPageN(page); + if (pageDict == null) { + throw new IOException("Page dictionary not found for page: " + page); + } + PdfArray mediaBox = pageDict.getAsArray(PdfName.MEDIABOX); + if (mediaBox == null || mediaBox.size() < 4) { + throw new IOException("MediaBox not found or invalid for page: " + page); + } + PdfNumber lowerLeftX = mediaBox.getAsNumber(0); + PdfNumber upperRightX = mediaBox.getAsNumber(2); + return upperRightX.floatValue() - lowerLeftX.floatValue(); + } + + public static String extractDateFromText(String text, String keyword) { + int keywordIndex = text.indexOf(keyword); + if (keywordIndex != -1) { + // Extract the date assuming it's after the keyword + String afterKeyword = text.substring(keywordIndex + keyword.length()).trim(); + String[] parts = afterKeyword.split("\\s+"); + if (parts.length > 0) { + // If the first part is not a valid date, continue searching + for (String part : parts) { + if (part.matches("\\d{4}-\\d{2}-\\d{2}")) { + return part; // Return the date in the format YYYY-MM-DD + } + } + } + } + return null; + } + + + public static String cleanExtractedText(String text) { + return text.replaceAll("\\s+", " ").trim(); + } +} diff --git a/openpdf/src/test/java/com/lowagie/text/pdf/ReadPDFR5Test.java b/openpdf/src/test/java/com/lowagie/text/pdf/ReadPDFR5Test.java new file mode 100644 index 0000000000..c17f447b69 --- /dev/null +++ b/openpdf/src/test/java/com/lowagie/text/pdf/ReadPDFR5Test.java @@ -0,0 +1,36 @@ +package com.lowagie.text.pdf; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + + +import java.io.IOException; +import java.io.InputStream; + +/** + * Test case for https://github.com/LibrePDF/OpenPDF/issues/1199 + */ +public class ReadPDFR5Test { + + @Test + @Disabled + void testReadEncryptionREquals5() throws IOException { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("r-equals-5.pdf"); + Assertions.assertNotNull(inputStream, "PDF file not found"); + + PdfReader pdfReader = null; + + try { + pdfReader = new PdfReader(inputStream); + pdfReader.close(); + + + } finally { + if (pdfReader != null) { + pdfReader.close(); + } + + } + } +} diff --git a/openpdf/src/test/java/com/lowagie/text/pdf/parser/RegionPdfTextExtractorTest.java b/openpdf/src/test/java/com/lowagie/text/pdf/parser/RegionPdfTextExtractorTest.java new file mode 100644 index 0000000000..ec13625936 --- /dev/null +++ b/openpdf/src/test/java/com/lowagie/text/pdf/parser/RegionPdfTextExtractorTest.java @@ -0,0 +1,163 @@ +/* + * Copyright 2024 Andreas Røsdal. + * + * The contents of this file are subject to the Mozilla Public License Version 1.1 + * (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the License. + * + * The Original Code is 'OpenPDF'. + * + * The Initial Developer of the Original Code is Bruno Lowagie. Portions created by + * the Initial Developer are Copyright (C) 1999-2008 by Bruno Lowagie. + * All Rights Reserved. + * Co-Developer of the code is Paulo Soares. Portions created by the Co-Developer + * are Copyright (C) 2000-2008 by Paulo Soares. All Rights Reserved. + * + * Contributor(s): all the names of the contributors are added in the source code + * where applicable. + * + * Alternatively, the contents of this file may be used under the terms of the + * LGPL license (the "GNU LIBRARY GENERAL PUBLIC LICENSE"), in which case the + * provisions of LGPL are applicable instead of those above. If you wish to + * allow use of your version of this file only under the terms of the LGPL + * License and not to allow others to use your version of this file under + * the MPL, indicate your decision by deleting the provisions above and + * replace them with the notice and other provisions required by the LGPL. + * If you do not delete the provisions above, a recipient may use your version + * of this file under either the MPL or the GNU LIBRARY GENERAL PUBLIC LICENSE. + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the MPL as stated above or under the terms of the GNU + * Library General Public License as published by the Free Software Foundation; + * either version 2 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Library general Public License for more + * details. + * + * If you didn't download this code from the following link, you should check if + * you aren't using an obsolete version: + * https://github.com/LibrePDF/OpenPDF + */ + +package com.lowagie.text.pdf.parser; +import com.lowagie.text.pdf.PdfReader; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.*; + +public class RegionPdfTextExtractorTest { + + private PdfReader pdfReader; + + @BeforeEach + public void setUp() throws Exception { + // Load the PDF file from the resources directory + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("invoice-1.pdf"); + assertNotNull(inputStream, "PDF file not found in resources"); + pdfReader = new PdfReader(inputStream); + } + + @Test + public void testExtractInvoiceFields() throws Exception { + RegionPdfTextExtractor extractor = new RegionPdfTextExtractor(pdfReader); + float pageHeight = extractor.getPageHeight(1); + + // Extract the company name + float xCompanyName = 10.0f; + float yCompanyName = pageHeight - 70; + float widthCompanyName = 500.0f; + float heightCompanyName = 50.0f; + String companyName = extractor.getTextFromPageArea(1, xCompanyName, yCompanyName, widthCompanyName, heightCompanyName); + assertNotNull(companyName, "Company name should not be null"); + System.out.println("Company Name: " + companyName); + assertTrue(companyName.contains("Rør og Rygg Entreprenør AS"), "Extracted text should contain the company name"); + + // Adjusted extraction coordinates for the invoice number + float xInvoiceNumber = 10.0f; + float yInvoiceNumber = pageHeight - 250; + float widthInvoiceNumber = 500.0f; + float heightInvoiceNumber = 150.0f; + String invoiceNumberRegion = extractor.getTextFromPageArea(1, xInvoiceNumber, yInvoiceNumber, widthInvoiceNumber, heightInvoiceNumber); + assertNotNull(invoiceNumberRegion, "Invoice number region should not be null"); + System.out.println("Extracted Invoice Number Region Text: " + invoiceNumberRegion); + + // Check for invoice number + assertTrue(invoiceNumberRegion.contains("Fakturanr."), "Extracted text should contain the invoice number"); + + // Extract the invoice date + String invoiceDate = RegionPdfTextExtractor.extractDateFromText(invoiceNumberRegion, "Fakturadato"); + assertNotNull(invoiceDate, "Invoice date should not be null"); + System.out.println("Invoice Date: " + invoiceDate); + assertTrue(invoiceDate.contains("2024-04-04"), "Extracted text should contain the invoice date"); + + // Expand the search area for the due date + float xDueDate = 10.0f; + float yDueDate = pageHeight - 350; // Move down the Y-coordinate to cover more area + float widthDueDate = 500.0f; + float heightDueDate = 150.0f; // Increase the height to capture a larger area + String dueDateRegion = extractor.getTextFromPageArea(1, xDueDate, yDueDate, widthDueDate, heightDueDate); + assertNotNull(dueDateRegion, "Due date region should not be null"); + System.out.println("Extracted Due Date Region Text: " + dueDateRegion); + + // Extract the due date (enhanced) + String dueDate = RegionPdfTextExtractor.extractDateFromText(dueDateRegion, "Forfallsdato"); + assertNotNull(dueDate, "Due date should not be null"); + System.out.println("Due Date: " + dueDate); + assertTrue(dueDate.contains("2024-04-07"), "Extracted text should contain the due date"); + + // Extract the payable amount with a wider region and clean-up + float xAmount = 10.0f; + float yAmount = pageHeight - 500; + float widthAmount = 600.0f; + float heightAmount = 100.0f; + String amountRegion = extractor.getTextFromPageArea(1, xAmount, yAmount, widthAmount, heightAmount); + assertNotNull(amountRegion, "Amount region should not be null"); + System.out.println("Extracted Payable Amount Region Text: " + amountRegion); + + // Clean the extracted text + String cleanedAmountRegion = RegionPdfTextExtractor.cleanExtractedText(amountRegion); + + // Use regex to find all payable amounts + Pattern pattern = Pattern.compile("\\d{1,3}(?: \\d{3})*,\\d{2}"); + Matcher matcher = pattern.matcher(cleanedAmountRegion); + + List amounts = new ArrayList<>(); + while (matcher.find()) { + amounts.add(matcher.group()); + } + + // Ensure we have at least one amount + assertFalse(amounts.isEmpty(), "Extracted text should contain at least one payable amount"); + + // Find the correct payable amount by context or by matching the expected value + String expectedAmount = "44 139,26"; + String payableAmount = amounts.stream() + .filter(amount -> amount.equals(expectedAmount)) + .findFirst() + .orElse(null); + + assertNotNull(payableAmount, "Payable amount should not be null"); + System.out.println("Payable Amount: " + payableAmount); + assertTrue(payableAmount.equals(expectedAmount), "Extracted text should match the expected payable amount"); + + // Ensure the currency is present + assertTrue(cleanedAmountRegion.contains("NOK"), "Extracted text should contain the currency NOK"); + } + + + + +} \ No newline at end of file diff --git a/openpdf/src/test/resources/invoice-1.pdf b/openpdf/src/test/resources/invoice-1.pdf new file mode 100644 index 0000000000..9074e23b45 Binary files /dev/null and b/openpdf/src/test/resources/invoice-1.pdf differ diff --git a/openpdf/src/test/resources/r-equals-5.pdf b/openpdf/src/test/resources/r-equals-5.pdf new file mode 100644 index 0000000000..b0b57ddbdb Binary files /dev/null and b/openpdf/src/test/resources/r-equals-5.pdf differ diff --git a/pdf-swing/pom.xml b/pdf-swing/pom.xml index c794b5a60c..daae6ea295 100644 --- a/pdf-swing/pom.xml +++ b/pdf-swing/pom.xml @@ -21,8 +21,9 @@ ${project.version} - org.swinglabs - pdf-renderer + com.github.librepdf + openpdf-pdfrenderer + ${project.version} org.dom4j diff --git a/pdf-swing/src/main/java/com/lowagie/rups/model/PageLoader.java b/pdf-swing/src/main/java/com/lowagie/rups/model/PageLoader.java index ed12c54990..e3bccead05 100644 --- a/pdf-swing/src/main/java/com/lowagie/rups/model/PageLoader.java +++ b/pdf-swing/src/main/java/com/lowagie/rups/model/PageLoader.java @@ -20,8 +20,8 @@ package com.lowagie.rups.model; -import com.sun.pdfview.PDFFile; -import com.sun.pdfview.PDFPage; +import com.github.librepdf.pdfrenderer.PDFFile; +import com.github.librepdf.pdfrenderer.PDFPage; /** * Loads all the PDFPage objects for SUN's PDF Renderer in Background. diff --git a/pom.xml b/pom.xml index 2d65973e59..183ba65bbb 100644 --- a/pom.xml +++ b/pom.xml @@ -50,6 +50,7 @@ pdf-swing pdf-toolbox openpdf-fonts-extra + openpdf-pdfrenderer @@ -136,11 +137,6 @@ jcommon ${jcommon.version} - - org.swinglabs - pdf-renderer - ${pdf-renderer.version} - org.dom4j dom4j