pax_global_header00006660000000000000000000000064147627336700014531gustar00rootroot0000000000000052 comment=7373c0076bfceaa88eeb3dad0ba00778a71d5ece H2Orestart-0.7.2/000077500000000000000000000000001476273367000135345ustar00rootroot00000000000000H2Orestart-0.7.2/.classpath000066400000000000000000000011411476273367000155140ustar00rootroot00000000000000 H2Orestart-0.7.2/.github/000077500000000000000000000000001476273367000150745ustar00rootroot00000000000000H2Orestart-0.7.2/.github/FUNDING.yml000066400000000000000000000001011476273367000167010ustar00rootroot00000000000000# These are supported funding model platforms github: [ebandal] H2Orestart-0.7.2/.project000066400000000000000000000010501476273367000151770ustar00rootroot00000000000000 H2Orestart org.libreoffice.ide.eclipse.core.types org.eclipse.jdt.core.javabuilder org.libreoffice.ide.eclipse.core.unonature org.eclipse.jdt.core.javanature H2Orestart-0.7.2/.unoproject000066400000000000000000000005131476273367000157240ustar00rootroot00000000000000#UNO project configuration file #Sun Apr 09 18:09:53 KST 2023 project.prefix=ebandal.libreoffice project.ooo=LibreOffice 7.5 project.language=Java regclassname=ebandal.libreoffice.comp.RegistrationHandler project.idl=/idl project.implementation=comp project.build=build project.sdk=7.5.2.2 project.srcdir=/source javaversion=java5 H2Orestart-0.7.2/COPYING000066400000000000000000001044711476273367000145760ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, 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 them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If 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 convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU 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 Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state 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 program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read .H2Orestart-0.7.2/README.md000066400000000000000000000032421476273367000150140ustar00rootroot00000000000000## H2O restart 한컴오피스의 한글파일을 LibreOffice에서 읽을 수 있는 확장 바이너리입니다. 바이너리를 다운로드 받아서 LibreOffice를 실행시키고, "확장 관리자"에서 추가를 하면 됩니다. 확장을 추가한 후에는 - 파일 열기창에서 "Hwp2002_Reader (*.hwpx)" 파일 유형을 필터링하거나, - hwpx파일을 끌어오기를 하여 hwpx파일을 OpenDocumentText (ODT)형식으로 변환 할 수 있습니다. 저장은 ODT 형식으로만 저장할 수 있습니다. 확장을 설치하면 LibreOffice headless 명령으로 한글파일을 PDF로 변환할 수 있습니다. ``` 예1) $ soffice.exe --headless --infilter="Hwp2002_File" --convert-to pdf:writer_pdf_Export YOUR_HANCOM_FILE 예2) $ soffice.exe --headless --convert-to pdf:writer_pdf_Export YOUR_HANCOM_FILE ``` * 확장 바이너리의 사용은 무료이며, 자유롭게 사용하시면 됩니다. * 오류나 불편사항은 이 github의 issue에 등록해주시면 주기적으로 개선하겠습니다. ## 설치 ### LibreOffice Extension https://extensions.libreoffice.org/en/extensions/show/27504 ### ArchLinux (AUR) https://aur.archlinux.org/packages/libreoffice-extension-h2orestart ### Debian / Ubuntu ```sh sudo apt install libreoffice-h2orestart ``` https://packages.debian.org/h2orestart https://packages.ubuntu.com/h2orestart ### 직접 설치 (Manual installation) Release에서 직접 oxt 파일 다운로드 후 LibreOffice 확장 관리자를 통해 설치 ## 버전정보 [Release](https://github.com/ebandal/H2Orestart/releases)에 별도 표기합니다. ## 라이선스 소스코드는 GNU GPLv3 라이선스로 공개합니다. H2Orestart-0.7.2/build/000077500000000000000000000000001476273367000146335ustar00rootroot00000000000000H2Orestart-0.7.2/build/ebandal/000077500000000000000000000000001476273367000162215ustar00rootroot00000000000000H2Orestart-0.7.2/build/ebandal/libreoffice/000077500000000000000000000000001476273367000204725ustar00rootroot00000000000000H2Orestart-0.7.2/build/ebandal/libreoffice/H2Orestart.class000066400000000000000000000001221476273367000235110ustar00rootroot000000000000001ebandal/libreoffice/H2Orestartjava/lang/Object1H2Orestart-0.7.2/build/ebandal/libreoffice/XH2Orestart.class000066400000000000000000000001661476273367000236510ustar00rootroot000000000000001ebandal/libreoffice/XH2Orestartjava/lang/Objectcom/sun/star/uno/XInterfaceH2Orestart-0.7.2/description.xml000066400000000000000000000013731476273367000166050ustar00rootroot00000000000000 반희수 H2O restart H2Orestart H2Orestart-0.7.2/description/000077500000000000000000000000001476273367000160575ustar00rootroot00000000000000H2Orestart-0.7.2/description/desc_en.txt000066400000000000000000000003311476273367000202150ustar00rootroot00000000000000LibreOffice HWP 5.0 import Extension. This product was developed by referring to the ᄒᆞᆫ글 document file (HWP, HWPML) published by 한글과컴퓨터. Please report bugs to https://github.com/ebandal/H2OrestartH2Orestart-0.7.2/description/desc_ko.txt000066400000000000000000000003751476273367000202340ustar00rootroot00000000000000이 확장은 HWP 5.0 파일을 읽어들입니다. 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(HWP, HWPML) 공개 문서를 참고하여 개발하였습니다. 버그 신고는 https://github.com/ebandal/H2Orestart 로 해주세요. H2Orestart-0.7.2/dist/000077500000000000000000000000001476273367000144775ustar00rootroot00000000000000H2Orestart-0.7.2/dist/H2Orestart.oxt000066400000000000000000016437751476273367000172570ustar00rootroot00000000000000PKxVicon/H2Orestart.pngeS\Mح >ww  Cp\;0.A  Ass[wvZך(5El r @CN]YsPLS$iei> ;wnU@ J.2>CҒ_QFA+y&*B3CMT+M%=tI\uI O3.a, q|:y'1#~{& o7݅oۀŊTAVD`P آR+ϗ_awi:#+00eE~_==̇aOc 0c_P"ZYժ(Ri!(`7űH,+w$Шt#v^C[&U G_<43*tM?RK[I">VT`?r3cr}߈'T6r",K)9YcwNsv FE5|~MЫ NηL*%vedK MSTˀ۳ "HcbkQQ^øRMZZ!B)4)}R5|W} |L:QrTch7P/c,헷35Qva`J/,T 7Jm o&ѝۚ:C`u9%Q-VQo3 ]/~::`:4L H9[qC4ɫ{yd{{WKBilB:`` u.[ [2HH jÖЂ4ŐPn3_ĸI@9pOeyZkNz֝nqgF/L.Cm taȽÔ>~[f= ? %_pf Vz3y(f$&M5MN~MM.W&}TץvG NCW0[@?SuMICmD4mNk2djMTX` ǒ=4IJl|yD=D(;GvY=[ݐaĎoM}98vx9+J e:t ccKtk5ddO3.?vlf${AXuإChYO(*P;,V䊉3ꘞkv(_`'Ε#!_ * b ջ|t$OG d 8?^nr2YT?&k_h8݋:T~q@DgL=W-bѪԊ D? e' $ruKUɖ萐@!ڂH{嬢7.ۻx5Ӄؔ;O6f!e;W/b,Pl >/5h{̾)`cg1AV$+6=ÊRGw 7AJ,mf^u51e﷛[l1Rs Tbӑ6:E- KKo5KYgd5ruD`"BQ]͇l͊y&v{9O+c^1/"16hy}wUL7I#!h-6TZKja3fcjtk$[u_ѤӶٖJlv$l˛E5\Aq<{Sk!^ay^L"vE~f}ښ|ذӃ[_K”}{2m2ʨw0:E7(V7QוxCqn+UM~<^PObXM)G0u[ˆB(~NЗѠչkɪ弎K<ԋhYYXiL@{OK8?1r<_.}|tS/$",cfU֎0qZ78ּ7CcHfIHT)49U7b[8-b۟ݺ]X,ԚH_ġR{I};FgAQwFf;(;%?_E"nG^I)9adC{rP}d\ՄF^ .ZF@V.6P}?cR*2[A=\̠< lrzP駬FHb4vU."yjq[[c|V zn \L)bH|M?R>S_2W܇n1_qˤagUL)|(22"r >G}DRw1)H=oFBP0T"31ab>v1&|3 Q_t{DwMCU͹iӈgMM%Ü/.Ew )1E: `rDwoF\8e.Vl@U!%C*_Ƞ: \_>~1RDbHi#mf}RWBn>NP#bz? Od@k*[Y) CV =Q䳎4-™t*;+?،ߨk$/وӒC 'M3JiES7O>!vY!υň4z/m2TWTs`U/z[]F{\=DOJ*AilժENkc7ڕD Ŀj0wWYLkP$`cJ%DY/q+/9T@zD}|Wv9is1eb5XRV6 ]S}h׍)YDJ˫-or j.'Od0$F'$,a9xΉBCHD(oz?V$7]-#6-QÎĿq6iIpn8/TV'O5;H󀈂_è].S躽! Ĩ)Kyrߒ\ܫ\&ar5#P  7Tj327E@ *ͯRhG-0yReHзSuQ_zZY*R2t3 |c"]Km)(ڞF-"'M`{fiů=&=HdHpܞKCQL8,Th2z bn_t{UIc扺TN*y妘eyZZ P̒1nݠYJ, HfiWU3(t:YLl"!A*p.p!(H8Պ]>ښ 4 X :jpV?BV~ jl .n x넟uuj NS۔PW|ܦ:BDRKU}=H#(kNyQR tl7o&; x: |փ JGA%}nVu̵4Û.=?Ly<&!hprA x͗w>vwxe 9qڋl*S ᪯ʘΪtq-@,V :xmXG/O`F] .uZ.ojr?*4Krgʢ$tH IElpNPw9N R᳤$u16D#4% DedDLdwW2,EE*}WRBכt.qBsk]ah6Є>-D&\EB ,ȿHⵖO@ yr;.jC3JA #XvT7xĺ#eЎ'Ý]Vf|A3]Y 9~guk+qh"e{bhu>6rHFȊS7!hnk+bb') .oG0 bާéWB[ Jqbf(R޼Y;;SJc4GKW49Y>L/&d{oqԈx 5)ng1lY Enu)^jn)",V> X cEVv 㘿 '#-flPZ!cq^@۩E%yt¿-m>p VuwD14Q;zƟJ,.E9v6} FϐenJhG8Mraps L?kø4弪> 2jv;.2Mo"mܬfؓ)k%L;LGV}+^̒C Js'}g H&Soq\f=}ntKнBb;']w"u7We<(Y&93 6Z= L]?U][x"w$a]r:'WPBÜZ4h_ĺT8Mfû&w[cL#g[ j RtSoN*DDUTpTos2"E^P C6?Jϑ.AyҨ [RZbTe8'+}_g?xw]DPxHcM`^ ǒlQQQġplAeIkCu6T8S!;T9K[`nH{/+GqæF)*uk&QNc?,"ޟg!Q/F\ w!RX8:(yƉ G/ 1ӣbQu=±$F`F6lV U:c*YL!"?g/t6P!DtlĦ%TC gӳw)YBv+o]PVŠѩff[Rp =ye`i@O.KήCJ Cㄍ@CFJԐnV`]c<`imO xԽ=[ o$9ޟQ׿0_p! ~}=Z1±Hd؉xIR ᒱ; awBK1!0kmS<؟L` w!:wP_u 꽧F㎞?:-5Zi䞫^BU!<)5ֻES6?Դ߰Va*͉ 90olGn L^#5b5Ak} .aq6Y44,E>҇q~lr*Ka ;JC]p_1굥l#}:%7,o`m?"Č F_o5广7|7.w2WY$rͬx*1kC.gThĠ&vUO" ȾT{BywXnQFքYfkyr_0)m61'o& dAkV(ujAit*j2:o [G1ÌaQܺQpD7zFpG9p !K +Jz@9lE,cӇڜEOQp&ڈ]Iȣ)[/S{~Sy/^L)N%~`v;3$`9А~cO@m#hG&jM+@p843I_ku-^ɤ$$o77+W1@{WSJ(ؔG65/Xy* dp 4bAim;-/ש ,b~k(ZL&3͐[=F`[9!Du]L?!ls+W<ӗDU<( '4$Ջ>,;8~p#RqvڡC-^QZl7+-=6&~-d6*h~r(-+w[יBf;[T[y -x\<[_=;3' a8+m\PV0<^"<4ӏ#Œ!%iw- !.PojLxy؍=܉t3vI5 ޯ~/շ_wL N |&R *T#lrlh=2Z$$ȫTUavGAƒ!-1Ul/=^p6GNj{.h ˁVLV+ tw"$>VGop{C^ΕЗׄdj7ɏ, 0)2;e%nǍ4W5[{ҥ yPհCil2zT 9_ԏP GꝦyXc*f~| 4: qw4q9iR=\!A0$3/ۂ8G8~+gRHIy5$'fA 4xc ФtYX1sȉc#Ѧ %lQdt&IEɽ*&@2ˤ 7:7a{/5=[q\ƨ N^EF4PK#ǡ U#PKxVdescription.xmlMN0ErBb`dXuvp 7`; P6l|yr- hÕ{GW-'4j٪ͩi|Z QH<tB k5Cz x%jy\|Q8kl6#MUyG0<|iJ<iyA014t'evXtQViܛ*a>O5m@S%LMjM:^m?_^?zшL'wWyQI:AhDw}C%[>p&n?d[IK~?e>[M7Rr,%PK:\[PKxVdescription/desc_en.txt%αjAާe+A zf΅ٙa& v:FBH^f[ɾCVw&$N˒ blT{_m$g\SJJDPD<*oVX#4fl/5(W-CICWe>ICQ+n/߿SnYofPD!x%4Bgj}z07=* ?zW7t4w2;<8gaY9YlT5IwQ{cWg{[Y{sq{'[CS?u 5/0t4mi$$ w} dƀf)V5)G@̖Z1XCqYߧ.hϖX߅I슸k"%*vK.6ýbK``եf$b@FoT+(`2&oec~9ی `떃Z.3~FPYfƣ+Wη֞ 2#CPη""y,`x[BYU/ !2mKEp%<y{΁/^=YALPZ20ۭ޿Ouڤ 8m5[ uSn"^jaXp!pA1f" LihH΀rR :D3yׯ ^cU:U}{!5(דKux&Hd*P"u˘oIB}XE$V`NV`"```_HPQ@ f`Hw$TT6*p3*nl{zit"`1i\uoO l= 1r%Rid-iWDj ncsr.qF/2Gk q߼0c$P[[B_&jy5Ll$Kds -[byU[C甤&Ďퟫe*,'I[eh^WlA@"pL#N/Lg#ZLd}+bw9/X 0&&.q(J'JzaϿX%?b`1rrv>Xn@~~&M]^J2{6{LDU.Nw,1pkct'/:Qr·JjUp9 cX{?ĩD!Mb6>>c:zEd;~ڳJȈQ+'ԙ5HU#!'L$F(elgXNr~%O5m$^61>׶+|<vj0?- = (U]3r\#4A RtJb t߬.ԼrxŪ?J?ׁФ AK:&p.Ґ@BS=~Uq38tDD_Gu{C//~krtg"3>;k[/G4dd`95 ֵ0Jjl4ipv8V5-"4ض~kלݟ\cLcnԤqz',1»b Ғa<>xI6,M~;!LfMcqfi=MG -mqdosZ0=mc2u"\Brm&~9dqAL%PGdiIQ]6 uUcrJ㦫m_-xNGKu$u7yſ#9ք3FVэŝv8wjʏ.Wgb7j/i$g?@4T iY78CJvq:6P`P)u 9O{Lf$$8yH>#G>i|+,AfCv^co%Z89OBNC:. ܥ_^ۇ2O.Cpw |)ԳH^ qa$]bD1/(X 8מ`8.FReصTE^Eaei+iԑU q[lbam >4ګFt["ij Xmu\Ĺ$ڜ-a\Xd˿(_ymwu15s15U0!"T-%rϔ1 Ԏ8^47Oπ0lHQEe7S[#(Йys8I]c콐P"cc_էs\ڏ2(dB?M$`6?狹n^Ը$ݔ>`>i+>">4v7s[NeƙyYwըȦ֏w@F1f=6$#AseI癛dTd:L)@Ǜ.ݳ zkkJJHE-Fu|[LlHHPpwsi\ (?PA%wA&*ߤ=D:Us5B _gj0E;ވ}bջgmdbg!5<ߴ e' rslsx{C\E_Κ\Z "/mmߜpdJ*R2b?:xi%1NJҊʢwNjD>XRC"\[Z@VKB8 *aPJâJyZڟvCWb_/t$ F[+aKl( |kmɿSit|> HuT*?&INNO3C& ̖"W*JJWIW>KƤ H@$E0I $>f18eȴ4##R ~1_NӝKi:e<{9ǟ̅)JS;n/TCo B~IWm}X$h#TEeiEvDMX p6x,!zi{"!fP{yЊ RA4j 6!]hx|ů@&Jv'hBм]&\̣.F}1Wih1LłP9ݗ}6,]Y(,-`9W==(7Ui R^˰ջ>Turvm#b%9fz8'[TrjDs6ĩtTJSZ;!٢e.BnJ"6BϧL =z'o~/LVXYs aGv&xZQe6?ߕ0Vbe&`92&/ yHv.M1E4CC>%s ;Z֡%r/O }‘ǟ47Z!5Zã O(XIg!xo ,C~uYͲ5 Tfr!.˅Rsu崄Q0ފ.gZmKnI㻬PncBcnh&i􅩂_AQѺ.je͂u[W[T&&Jcmr bEpjY-.~1&`א!J1ЫCX9D@2 RzNnB)G/G\_O5Ṧhu*0{V\L<OWͯGlꌥ:"1a3'W؆C&A|f Fe |7lYDކ0.uQ"FwLxp=Rt-VդҰ M 򺂧~K \]2-Ekfk4Wio!c&}@'^E"|ЊR6^M@AHA&ָ.Ws$6woqAM!e0'q[RSڞTNzH=H¡ dAe%I4K [kTҴ9u:Tk*4υ?զ8X^>|wg79x/:-zMU`3X2cwJqB7x`:~ ܻU_#0Cp@h(zjbjY .o]"w%crBPnB<%b[tL\¢,zr#]Lj䭭+o>[./;?lվ$s9q B,Zիz/V?c@Hy."{v$.m`p:xA(Q (ʞWE'{+g,.cVU&iW1.2iRbKLgdu~)+ h-R3$Y:Kxkx{ R2q({֓fnkQ__ B+RYOQ1 RY[v~){*xEXr5)R^HR 3X{Զ~QTV]$Sɋ n||1!=|eIDݵQYl#sA>,tI,Zb6 tI;&y :n.2ausB(wuߟ\XBh<[XKRIh+g\D@ dWŽۇK ŜFoO-zW,\rˉoﴜ+F *BetIP ABio3xT"%ݯ!ҙmK`:Q<~QJ:C`gm/;Khp3]w#'x釺Q'Ry3.5ߩssNr(NDC'AE͠hED8|b #`ĬE3Y셡QzdHϝZ,jҧTHP & 'h7=}x_u_*g^5]O w+ =_{u]Dq>&Q9^%ty)k# gMF;>lmlEnbKcm\ "=҇'ƊT;6 Cܠ9} ,+&7֡B7mphl=Z6XZ*8h7\vm#ዶYx!VhnyGEmG~I<բmuN Olǯ{^&ty=&d*2b^*N/@ /64Kٿ)XnӘ{4{En ӈm?8+,B4Wn7nS=phqCܛV<ΰ1} fσs^Bc%k. .~٧ jyL)i߳k~!;̬\ϘYTRWl' %YV^w<wzoߔMvsyA| m↦Z/bvW̳̆PeY:t?0}V]/o@P䇗_^toC߲M}F*  1 QaC'bvb)tڋK #iIH4+:addb5БoEUhMcI;,]`UeDq׷RL33pё,_utls8gi{9ӿ= L8_QRr#>MSK)_CGU{N۹5w66[}@$k^DGB3MPFU+J$U1S<& ~۾Tqnk[ٲ|uNLm 3b[R ( O2yCiD"[҂//uMoAޢ"9YI؄c5 ML*^9Nլ3L]oR)~X~^׃r G̦?&0 :HdV($;,7 } y}rp!< y3P:E͘fg M !]&Hn6O%H"%t|%@~n# vjqGȨy^(}16VH k⤖@ (&BT;H3%6J=PR\$;T7QԉoZXIkY, ֏F{jWΪkQN.uF3䵉37 9}Tw˖9aWLW?rЎ]M+XKB+/^TPߒQq3{ԫ-Y}mFn`#vp8`tỦK^c@3k-bYJYõ -0ֵ?kI{F*׺*nT"Qfv/{,.蟁ӥ" .7EiU2q#9#+̦]48l~!i 'dP/Z=x4]c6L'K/Wsqf jf/.R"3=MҘqMUT5~ܘ8ۃT)il=MQʩ@zm d߫'`|I:S uiv*#wݫ=P0J*9J"?TKiCa]f \FR Ke4l/e&"IE8H}`3 ff j^8n?oXږ MVLc=F 7U^/k& QX-/?(y-tm!?&. 7J/[ Ynw) F[umX#7_D&uV%ioDr`Ǚ)NM'Rd Lge$O[bv`;Cyr;%uLIw%TC;R3L^Zk tgbbuzibձ5mzíDWĽ *OJn}<#55X.d ɿPj0ɲ9i,kcz1#ӡOG qg{=}& =hvg6Gd:T{<`w,b8/ƹƒ^tr4bN%nX+kY]&!@_2h\4 .䬪z~0̨'!דrр8B1'@K$?]I5 ? \e7p t_>6ԧ%g.X3bc|=ۈ?r㚟ރv ;0"Hp ~#!:yqsU3UJZDQsG J{ju BDA փPڂ,VXDܑ|"|*Lk;Vg[:J9:y> wXvk`t;P\,F1j'- .1XcW+V+9."<0"nD3VɎ- { 1hsR߿ljldєw?sMLfAK?2tx֠Վ5UpII춉*I*gSβxgN (g W7=Mz ySi;'\52MԸ0g6lIŧGu:7$3Ca@}EL?M̿P9=YL#ÙP&ZPwq.6xKԁHDsһJX2In`O/I1uȲ tkV GPTokխ: fgq+߼=[w>F up!Fw4R|1(ReqX9F!X9%>! Э ; WV}J{2Cח7ƍUo$*ͱ7#ǶvauS1j 5Y0qR3`{uZJq-KU_v=E|{mh׶#4Bo 8Y: Lylv'3-kyn=ldL𳳫H5RpuO݇}wv_hcу`O <{/)hMp ހ Z"CcFQa_7boħfs{ղGYfwr мxEPO_U1j"#@#BP BnUab)|1a~O| Qcx>3Ub.Tb̃9Ii2H%gW ɘK%H>P*cVi2sJ]L1T) :kA#mSYLRQ@1>$ p5 (WS!Qaf!7(7'#F3Iʭ(i B_f63V( ,"s=f5sx^<Fu{BqlaSy&qk:%\NWB ֛/FI\Be08NRM Ur},' QR9[0'|CקJkh46[k [͙%<ܿ2NʧF6 @FZ4ȓ" cDC(rWaBX f-8rkQ'?@qJ&GO BL >9': 1t*n>Wl A8xYl˶FEj~ ke hA/ 6 > Q%lo(aز1xMCjy]Kl+m c -ǠzY'9hrkұ 69n0m)·!cER[dm$"A4ܽh$eQ{2GgFKhڨD/V1 -'iS哩'[+,9NsۄWOi/N s@y`1aexX DnTNO/~$/zah j߮y=ÿOjNCfKOK ^>}D"x." "l#DQ ͹K_r>1#DdsE| 1/\Is̀a<eY_'"K'A$=<;REPP2 ,VC7qTrPEB}Ѥ)cփ}/lP3/,GECw S2-P9MǕ/& !cE/iV w{D4FXd~!xKX,|_ICCV,25,/EtDzaD, jqll?b"u^$ J ;E#HVrMIXn4+yf3|zttp1d p }xb,RK >/,濐oD"II}g/"qHhbZQ  FKZlGo:Q3K@v؍`^`UlG DVˇϧ/_w0`$" .ӓ f565^6K~bB5;U;j,PoZ?F%0%uer*/uzMުBJ(ri2߶B۳9F{"z;_Hڇ$뤠5 _Ϝjq\s*0k7TXY;?#[Tdᙠ5$RRo#8fAq{<`VDJ:&:(?|.m 7LM o 1IPNMNPNOH&bu=BDѽ(! 4&3w4[2#WKVP*U2M1blX\TK5 /G-"]ûc3!Yz74/G n[.d֬4=W:&ᕷTL*k!L7WgiwlRghV'?//,KM[4/t~ J<@m\N*X\rN$UkWk^EK}gku۰)LRuߍ4]"w)jv[[[Ϋ$P W%Nq8кrd5ㇽ,fՋ7EX3+F;H+8-@ש-*ϕ+X '/6[Qe.9zjo-&Z9AэY!bsSK7fb$`MOH=QK93E'}Pk#hh2~~)Fh.NHln<8gA@|A|/DE&hO/l-I?=t+ DQF| k V'Ȧ%v?zKrs!k5TvJ|Ks?h9fbSD\|cwHd";UR.Ep$K"ݵ`1g?L#{+kjG Tu9KӨldEaژ[R=͡!HZW."ǒMDk~}:l*g:ʪ{*kJٯ2H r>a''&E>/VZUJBQg\8@IJd)Yh}iʀU!j؅aZ*׺ϳ⎧6ge)3#$:/͇ɶyzkZv6ytʇT~217 Hǟ*FsJ灢>5@ӛ+Ƚzb-Qݹ~|mMApQ[PvZxmXH32sX7p|AS\Sh*YjDa Ya ! 3Yv҄svziq l$R!@)CAnD*- Jv)5T :&*#S.ݞ$ɉၧE0 ѐ>sm${Xئf? pnºAN3%1gj5fXy$:->~_ؔsX\3:9B*j]aN R-icFw}ˡXW8ئtr qptFӷ=yR,}s 8CNcd_fYri9ZVˠ[\knqdZӃ'¤g.}AR]kNgw!C/ǜw'7/!"WPTQsTo6.at;5!f2]W 0b9K5d? Ec!/dF!f){MC F^ Jt5Ev.0\Ui39YJ%GJL ?{%soNx|(dW|"'.d. A^5-T9LJyLu_%d K~79Uf |T:GߋA 7]nsþzm -;uq 6DlبW,V1ܒ -+a')M[#XA4/@V`7o1zGg%6Pk87g0F,UH5'%w3'_,嚜HjBh9?Y( !QW$3_o$Of-.YC"S.(`-4i1dH//R . ʶaCF_FR۳93;?og~BmWèމ F =Sjo :;]5|1h{zȏ&#'LrFJ` Ƃr$9eKy؀`Xɍ=,EO6V6qnƌ,p|!x1 INdFJ.BKǫro/ Iyy(diS5 aYj ĸ1ˑ' ID!&h9X7XLJ2m=!DllQ(RqwQA#62֠=ةzwL͗5\3eD@Sd B! #A}9|0H8)?yHJKH ZjiH%T ;?P_ 0?l~.zO;l򅸥0k>E}]vL)8v+e巒(A6P qU3\YYBI1h LyVV΍kaWF.]KҒo0 N'ww+Ks&yZy7X Ϊf3.}RX*KY3<|r}Č"; `dҦ:T,y¿ۈX^'Ck C[[;k\H&|@=Q0 0M`_dN;Gm.ؠ(mڂ \_w":))%x6?1R^q'lǧp 'ץzDZ^ W{;$`犒bcR)-Z-6+6"+B a< =)kjF >qeKM:4C7tuj镽hҥa$l H;@yF/,bbQAj!(iSrgh(W&4.SOP~yz6@o{]lE3-.\9Xij`3:IRkX #7[BPߕQ"c6pN:ѳmPc^mHKUEd'I 9O"~Y_<H!őr:Ӧ.rFYzG]7 W6QjF#4sR"jVl@/hD*ƝB dز%`28 v2ø~]CCt`=Z͍ -m"F(롅N?N.VW5mahJW){/ X;}g﯀U)I֐_=E4u2ĆA{uq[89{_[֟F@K+Kw`o>j}$T? >)$(L*&uk+ a@MAɶ1Q<pXǐ%YQIFEl"]tu0 |Nrn5@D?F+FGeԶAxVѵrVWnU g츅׷qsÐ7%],)6(Wny cYŸ*AEHT50.#VVAk.%\aǦ‚3p!vGDшPg$h@t!]>~>`f>_7;},/pLhzkߴ=x%_8COyA:0.vѓA1TxHWx2aJd79҇xcb^ҹq{ ᑰ!8o )b*0I=V}ɦ{ օ=;w .7i%y&|E{{ ]6+v9)S0b8 ^q=yxZS=t%Tҩ6VNx~(x/(MSx~tuPH~XsBLᅰ0(=+XSIv*L9N ueX5%U/cbu:D0E -"/ތC|*th68qnWH"ա &SXrM22PK" 'j+MZwI{]--EDӡhzNy&A c|c钞)81-0wm3#$5H`rʯo|`AL~#"'e}fbc>Ia_](}]WjMG2i&N+Gv^}T/fqP5m*es`^_z 0 S ^BJD LĦ+c)z"FMX09ҫHX))?'Fk VC,vB٩ X֫,8;I.i`~NUsjߑf]'14XP=Wdʮ yl~fi~fn8ɜɔL 3(Ӗc8IYr aݹSK*H)H7&|aMVMj-+*2}*Z+wmUN}zj:~P&/pӨhN_C2i=qeF(0*!Cy<7pAa&'L2|YNlNrĕ$k,(G2R6vBǘx 40MbIR-B4'NhA!>yR4+Z4C(R!¿U! P y)zUpM. oD)3rr! Ī鴺Yetpڒjֺ\gJ1aW-#~`JՐxfCxVuMB/RG[GvE# %Xbi*y?n`Y/2[j׹eh.PayLS!p }n=ui x<-TJdO'D3`pfrRkyiqBB0Y1Ky.r9rFPxZZ8FՒ&NK}Z99d)鮔Pwg9z8R0yAڞBͬKi%~$ռUA Be<2$H<=*a&H&Q8ylhOʆ)R!D6ˍDyZeXPef+EIR/ kwSN)AoY.^YEm2o^UIdK<ॖT6gŖֆV?H9j }"o[(i.?5DV|*C/HjjvjZ]QSY \8&-?&h_]n'Јo<+>9??voBj֯FB)re B(% Gkva Z(isc^XM&g~jCT.lz1\Ud'uY`3G.i>0U2n |Y`l1z<`)9{\=cF͙=bh ,jb иS;h˾}9x9Mψ]Ax@Fҟ|Ŝ ʰ;[ca@{r(Ki2!Qbo!uo,Xzjy`{-Ǧp>VI}vbv9aU(>|rO+ӒǫNacpFt>Q|l4§$Kئ '48 /dӔKlU'.ۀ}]pmq̳/OJP̆2P%Y3BLPeS]N qnߞWQ`T\w*2Ig/F0c}lE`)z c~)°ʖ59i"3 p=rYc=rPC]vYczJW?bR=S"{D;AS';:]E5>A2؂[-( >lany5 zx[UEth}!'i ')(xlQ@(^kFY'[(mɞx{nPOY{XIhh=X2bQ,xSX] ;?̔g+t1KVmWٶ xbȈ \w.ul{f <"⸩sGx󮇙. `۸ngeT':f`tk-0)ՕXg'$8n|>5Dq!s3#y Ovbm=>{^_`Y@@UoZ}[>]Rݷh"!_mwLl4rt@Q|_Xb1rQy8FBu >fŭ}3a}oNhrtM[뢯48 FԬg7T$h ЯEhbZl^⚟FqxcE|'-<V0LIW^"@U?}U?RWAʊr@U3ZQhC ezR.#qn@r:`Gj Q嗸)_ QNa;|̷]N 2*6?W\groBnS2zQ/=mF[#0Y Tn=vJ矻(dվ`84K`Wkz$y' 6nqO,"^ MM`eX]vzcCXW2#cKhNG$㷿D281B;:*q>3Z!;,P.}96< ?_0![+frZN(\1L)m [),k腵PU+wkU0-i[WCѫWQg ).яGXMy\h6 4 AZO~WTϟj)^Y5cZgVn*C_e qގ1d]% ?ҿi]-e)  tzB{uvY*Ĩz>ș5Hkb ,ܻKۦz2gڜ$j:f.CzKE8RJHʡf33=yښYs^3?uՏp͖a v|@Hkx LfPi0c6zܵ;Bz;Pd\vnGr`nXAd)ޮ>n]cq.|<.5phHLw#'ZȃrwA_]7pۜqrEԇ;$b]z/d\4"?}7/ŧd/!W»{kF!uGtkgm`&N5 {ēW藽j՘焬sD{PT߫~rw>?YH(39QHPT#z#z~H@ hDu q$ BB XK$Aˤ+皪: LcaKϓ;'^Lo7x0VOO̫I^HkΜDp@4P..Ɲ!WOD1WWIrq2bE:Y!52E^io?:Sakp ;_Hzi!JUǡyXԃM2߸^JG4v[`da+!::VfkqGfa3F1C$X`GJ9Π(jÙM_O`ICvZPgM1[cTʁ]yMl6^L]?t-FB9knrMuQ Evl6zVbLv2 V =\Zp@q k~R jXUΕȅEc? y վH+0]:( 7ʒC.][ f8'C 3)pDه;O o#fnL4VKk"ftn9j72Ci5xpz*x?lw,Jw^<*O n=j_jMi7&ioG굡B߇dF9U ɌCG+=otF{>9(=V$|-d,w+j)cjO@HeXG0fb'&$]gu4N]4Oe3u5ڬ|#}m.K,.(sg5KkVتcdke1v6ꍺ;=-<m4k+k"sZ[,=}?*~a@aȽ9IK@K5+Y*Vױnbd[.lr)UBD쭯:!ʊ|K`g Pk͏(G8C9C0KMJ۟1@P4PLeAWJ/ev3 g'VFg*>|ڿ \bQ(U1XZ h@5C1gjY .. +-U#["#ȗD.pg(9;lS9/L/iYaulȨItTۼg<ҙ}e?/-2G㪮!Mby! %!M`xȴK%SQ*8&j!~ }&NHB:Q[t2AtPIet8bJ~$PH|*h."ݕcĠ#=_/-J^ڽh [)YݤXƔ%gDy6mzi?&TvfQ^]"u `u1K{ayx5ٺ1zR5_{Z[W ܿS9=UAVֶERq]Ӝ--.ld^D>J\3PWTܤx>+ׄXOHitږLEz2u8g%X6rzYmR6@ 䙘7s4lk eܨBQ Uۧ^yzn"hSBL//imnЭ]Ȕ FbVsh*C11-Mb IԻÑQs Cfb#/#?DŽΔƀooj7鉇NOOmWv.Sݽz 3 Z-?L(\1!~FBe1e2^n #u`s՜x#]";:UMz^lB_y?#ꪙhXlSHwlh|PeK]JQ^&Iy lMgިY-R$5`"}1{}e4+1MILQ0[S0\0]&:Oy)z.;ooj:U==ģ1FtAe.xNQGQ`rK\h -Ab{a-CW F?6vEJY+:_{xGyjb`},:V$ԞS]kb/"Gb-ocz}h̕wqkjo$O'oD=H^{iܳǽ>e1,ʜ]{q;TJ9.ᶇIX*r{wCGMO:gRV d|)wx'o7σڠ>& ܿan=wRϥ,aojr򦢦2Ho#Ge 2eaN~205 ^R DȜeE`jy,ATS wKM#R?FwugmxVM%nw[Q r6H^Q" b"r8=~:x>VH6y [?KCnSfCRZ{N, $ Ε! grYic 4&S~LFؖp*[I>tZB<wXrgnHHWB¢z;&Kt~~+贳nr{R7/Z%*I*o=F R3WBuuې!􃆐!!Hu CC_!!d(̃ rqrefzfzfF%scuHBH&s*1JRtSU`ϡ1!@aٚ070 16Za3np?-|-h?0y: # :+1p$bʜ`2e-q۰W_8exC:}ݵi;9ik ?K8gk]N2?YA`|Ђ҆đIЎӎHka2_,VDuav]uEUy!9CVh0xe~K=r>]:?+?6 zYp~bDx(WD_sqBmZfri?{*Mݢ]žȳZ#>tayxt@s4Aw::O۫h#VIAz|V{;벸 *S[(佽᠇䣹mӕ dUm=r^~iwAX5暀wdeo?Xzz|< )O6GUaIEf3a+FEeXo 4_>#lb]ݙE1XkI}\ؿ19:'pۤpET<s%sD \ӋWCD;3(Y앎1SiVp3G6Hcc!x_®Z9[Y+z섾:L+t(|h',yQPm(ȸv9h\O~e=42BR7y|\a ȞRܝh?f Xf? CA=h^ߑ,I۴%Y2`Էj~LT,:kDcaعM)QkH$P਼S!;n8V=Jv~KƁ5 P0$Q JPmhWrn i˚XEpSYG~? {u=hXD#<. FŊۿ%C@CO4eu 7@4m]zM{aD852dj#5RqESqE2j_WD U7[{=M$ɐU?ak}ML H)^M{N"#v"}PJ>2x $"!3XD|; ؎AVP gSs\_n7oB&op[GXGdZٌ+hj&jj4 (IrcM+>؄pFrs.1z#:XjV婕NI~}L>TBmJ"n3N'_H&_o̰r/*Ņ}Y1wWJr/rɇ*p4fv{MN,_?_/R@Օ*G`]GeIgnt RVD#}MƐm EML`lZV ܯ{L*I-2Y> /r.WX~R_T*WWHXq/.p9>ʅdK^nPq*u+kK4Mx?6U+5>JK v1u|8wHXΒU0H#xeaY!rvHw,qHJV ^Ls(Lk|x#:oe|=]< *&̦p](ӰLj[9LpN L7U~AJ0xw=~mDyU 9 \E7Ԅwh<=¿},5 3TR''*)j)ϘlXg wh5?8eapjL7g-*|z"kD5JIQ#c~ rwp=5}mw]揫OϹөx9+pAquuf5 {E"<_^ Um>ܶ- \bxw/"ɨh+LgHf *³23o\=>2G#}ߙ\F9lӑ-tV$lVCp\l5PiK^PNWjiJ& mٝ ^CCaJ5ُ;#'JG#Rh FKz)sS$-6W#Psad,iE%YHqK3>']Hcko!Sph2(Lјԗ"::ݣe?_и~ߩS*^sahIEVQf"Q )p&R|ԯYZgΜk#fUQ|){ )ߋw%*'l褽y9h":cF]jCw2ۤav]`Uh:=Ez;Kr GAnhI\74:]4"w0gbUiúW^1^41sY`1}lóڃNGzT9VR ;׵Qi0_'xՀ, 7@W E9pXH ƃv~ + 0 As cJ0@sPHX+X#f`7WԚq6 9]bM"˫-_j& d";\, s3C67/R46*:`Wvo:;9=9ƴeCvep{p+ԭ8L~z&_l&ǷpNB~bEX(jQ`툞֒~ e(Ӏ6xar):n#!;Lf3&g2!FS|jIGɩ?e[)|炆q2FpSd]#(bՈδ ͌lB+D,nю vM w!.3'݁MziJva7Y ٬2UǹT-Dy3WI&"cJ<ՃTTxqG!y4`[D.N1ZC])F FΥVwUѺViy+s3MC m1Bo ƱM}4•o2`osOؓ||.}y79T ȗ `ZR[i5k'qXɒZKRyBv"gT`f R`ZШeѥNԤ z=@4F)gp7cfO«b2NI@De%K9؛shJZ+,"WBɛСf…+Q&)DŽ עR%62;չ "1B w7ug }t BƝiP\`~?;6,&ft <=E9 /vgXg2׹mxM$F{bv))pl,glu͘^}m^8iD uWLKnC3~au#8ċX{:nXA[jpǦ vRVKgHހv3(d%*: dOW8GHl$g=fjs;欇1C#/!ɋ-[O M1Dʟ>(̕Cl̕!ት6!%j2B&2 C]CftPD P22ֻe˚V%Pɪˬ2:j=V`eP,OE%;l,q}j>PO%Dl jNլO%ܻRe;IP2&|?InY*k"p,믶*7^CAeJ _/S%=2m%vb+s%n4~"EK&7Sm% e2-Y:wP$c | 2 &7N'\Mdz> wQ"tv$ ܐTXB(N}Xr xM0I208(#iU~.#ΟZHbq?ai4ӏy,QRLfb؟*=Y&4c>y\tH|!=z ^SlLB%|ANI`n/f7І~ۅ:G FqWoKP%ØIS})믦 nޭoRx _&{*z&4 -6TƔg|-VK{Au<wBn:r8O!`/%1ij!6r7)Ո$+/tۡlN v\yujoW uqsR_h5,vo<孪~vԐ/)zʘ|Pf{HSB^ɗmS5лp(yfrOxf\+X6劶ʏh(:˶]z'N}$iGҫK]j?.&NT(oP;Y"I?1=,?dhb ؅W\[`RUʩ,@ d,XȃM6ŭeɫyLImέ@q)Mi+B*S@6ߒFCRW8-Pz pa=/PAMmZRA^4%EO*iFɦ`Ÿ`],~CIX4IH_5 -g mx?NO*DLi/u `4qpBC*(P y}~ݸ6Vm^# Fכ՜[v';1o7y7~'VB~w >p{ мtxŖr^yԢ Y{qxqUnth+hyJE^wc=W90΂m%|3FV"Ε:yM>g{cD8>uH/@bWݦ:.ͮ "hႊWWgEm-rM —zS#ec]怘J ;/ rY P}nՖgJ{x>/#H1)-'11J$\e0՜]% ?e>3>a<_Xa.B&DQ#$ eNxmEaF"Q\qwR-y""nJd/`AM#*bz*od /'˝؟ F%{)7>Z%_7ry:Xm_g.}N_>,Mپ?gRƚB ‰]d*I8ߕu*!Dʩ c$N_)(W_qqe=k5tg !5̲\[U=^"[e`e9pYݓ0oRQ-]a:7]Hy1=ә='  a8n*e:*}ʣ`߼o_ w:3N_7rM..iTugSo s"p,ƊSD<,BYZXU̝Lqqmd2tpN9R:s$9Èjû9cB9Gte+B[)6ϧW+՗[98]so cNMӰT)hhRҳ<_GbܺkZGWb,x>·r3TTԭ6-R鎺S.+ŦTgJ+m qt 4x9u'f#y#vd!윺>4vx'oВXįa$̦ bESQR ȯhS&âO"Xy+zoP7umWQEbb1bS SUî[p}~p`r¬ah Ӎahu5ьvϊA Sb/nŗ-'\D;뛡U"D|0J7дџǮa #hDV{>;9))g?mXvL^uun.ZiYz+w.qu:#0\F)%*xMי62,ڨ4+9\HbRu#Yo-#d&Ќ;@=aiHkX4oOc+MO]->?n-Pyu4🹆+|b<+x& S|0x}e+gT9Nȭ̌X34e쳩h'{QIl atWNR;dF߶ . ?6/l 7c%jwr# \IBBRyLdRp/NHy\#<߈Cxy*9[U3˶Yn:-bI y2UX!ѵgFBiY'u&)-^_|8rmB?NzDew3ly ߲]LrrHF(^$̑RӀi-r}s5BߥUS7$07^+wi~E;9dG GZԄ̺NsVKq]!;)/Zھ_`8-He \}Rn%6Y.RPũ]p.Doy!-"1rO*<,N%&Dt*65ci~2Q`e0O55Ksf-.>~3DWn.~xݍo n &\-~,~Ը *~eLOX]Q3O_u5i[xz2^:rVaŹO}!|jj!ôSwxM;2Oγ>*' .Kk3Z= ϳKC:{wFI/$WkG1r^NE 0GI#2n!s>לwMz J]F0Ȯ#3qJc)4q^|-.C ژltcRbp>p-&բ4#-I656PyBC-իu7)n7U5'͎ aQmv^qЍ:[pZUSuXtBeĿQ0!?RQ5ĵ,>A_9;~ ߘHzŃE[;[$-s D??O?2)D9?[: h劄`_fƝ&,ETLyIWl7 ^LB}A.ϱ~/; ~#Ex?\ +_yAz(]ui>DiXLFz4;Jk7Yu@ Y^,>9#g:U〒^mUMn=ii&^u9T5mZeN`&#^~aB0&ցS2]fW/ݭ23|@c LHhH;8d} ("!8E$ABٞ?{O=?^f=>u㳬(1h(s5v" uQU4 uN iMϙ[.ah4%;l7PM&ZQ rP0Y%KieܘJh[Jl.ev7>(+S>U8:]k,O7VmPMh+dX( "rvѼQ"@/&fKn~u1y#bP>d|!_=O׸Yo:0Jґڟ!:m"f"{tkbZO~RUY)}J2^קlsDA߯z2khb\ 7;ux\"/Y࿘ ohI*`E-2 h貿h6BSf<#cWIڂ|0ξ":zGt}=}|oB cE1յʰmp}tٮO7{߱QCnNF o՛Ũ׺c*E=f?Ob7hIg3T7Ck5KTcšfl!FƢ2;$qh\PטlZmG< X;2g.r->X"ψ<ӍD v@r:yŕ?ea IJ E` tGsrarf0r>ξJm gDm,@bw!C} |Hyr;aHOrٺ]q Cw 6{+x4am,B$v)RLۭvyaN]tLX/ 6„n5eUJWXu[C(R`@Wm[yF*NQ7k6t3eWXWmJTFQAqf8GsݙV5(}|M6}(Q3?ewصowt?L•h&ȐYT{ ft44hLTvxo[[Y,3FpWYV.H3IuId@-RzhAc# Gc舊㽤n Y,4<~<0gJy3DQvPqcia56PcqELSv~p*ݰ =S,D}TVǢƣϡ`(~HX.BA%AIo?/~23n;J`GeD.x\0z#"ى1+ 3E?8Q D?`;d>Vr~y?2e0[,y5}wx@RoA__(8Z'| n3g9[BJ݄2G1R11R1B|^}ytQOT_5:~~Ŀ/llh@NOhK32y澾#T붭.=;o|x+ߵyrN]/ȕsNJ&W Z`{޼1 "Z 5L3ي;$󀤆HA>ƽ8p!Ws)RFSqv++ܟ3_?oA Zya!(1}qO[DCEh8*/hLhRUiGY?|=~EX@8lQ1ݘL6ָ#/T |ccEKq Ͱ]Z g*])cD*i@}ZЂ0zqf9r>DׄJ[g$oQ@ǑD2ey&;6l|+Wg~ETJVg"{Y b( et<+̟O;K+?uNI]E=Slb , l.i|Z0qM]s4:SH'!?"o++!ֿ%T;yr:L[6"[`)-i1H{' 6Wg 8S|3~ǹ };D1TATsC ;5iۋOE{ CmN{uasre?ɕQNbF1.Km]X[+X*slq?oS;kPI:KG=qp`!gH6vupCUCdy5xC)VvU(%2_~5"k-^S==vj{ֲts]o =9MgBgprH pID,MJvLSu\OqMyV:,'S5AsF궇"K[JwJo+Q{"KCJ5VCL4nWjGqzhDuXHտ8?࿠f a=GFGfi{TVD"+vڸ*>Zh10ͦ۝U_uZF)r k}.;FZj?SXp<>DBq'EΥHaSFGDǒHU nvUP4z_a4)rWW?.'&RλMV AÆ&'nGs Ml0y(0B5ˍu)xmS 2pr){EQJn~]~sִul1O >P\t)A})ɻY%8893D8}>? c"=L{7HiɆPSN0]&s WmRgPD ^BEak(b!*05)Y-|+N .{ ߘqW {OY[Rm[i,kG߾;oe#w{l&,EVn4grEgG)AL]u8qJ:NmC+]CnC)I I އ+K1 Qfn (nU+]XK$r N !hIpuΦҊ,X} $࡛#Dk7C'==HCͨt ^al)lq,YTaYb [I8p+<^5&j#_{G86V\G͚ s-zKJ3*/z&`79l+?JLgaOMMƔ=f*5+v|&lͮ% we\!6T^{F|g ȾRh P zғ 9:MﮏvYi|M etqȜktU Z} d/Y3i:=)4:=52=fb생|3E.> >M&#''I[;ì)bm{V9s)GEigX*5l)i&#:Ol'#>=Dzq /hK@&pQ1Af%Ҡ2wF3bMBst΢LhMҌ~j!(+7eg]_.r_P*ܺ3_i]B)f7Fr˩&FXH˩:I\irrXH,hB}l}&_,Bq Q;9K5Prq/!c-2Űz=D3@\R"/6[vi''-%&$V-k{\2l2:l(^aO0`:C&?s&Biף9/Z_VݔcW:frS%srɄXٲma5N鉑xOUߞW+eVo#V!CEAMψ `PIÚ97T~22eɁ;Na^ԎcL1G<TZ+?A$ {ݱaC;)e!sP%5\tX3 oJ8kv~enI)d[9g;b"KMJa[hd|"zS.S@&ɌK s"3П~X'Mfj=Bf=Aϊtpʓ#AN3&I3g=3- б,3d $%fvqʽб=+x V@!BL°f ~ӁPcdbF1( j e; (`RNJfT&YYKfmL"CzQ5aMl.65v4z)RX3ҿ1H6UE_GFMyX+ f8qK @@\4Hk xZ)=kj[̅Q( !) }?Y>]LG<24A6GUCXPqTP Dij,GiԋpkTjEZKL>űH)-qYu;Ne27AæǼITV"cC _w&^P,}7|0t9ؓs+ zRd;GDh6;wxL2+jpմP]~VIw)چd7p mtLGRefC n6U ̗htǼQ-) ,YN n]EclT+Kb~N#^((;osE) Ԅƭ/KVMd 3;B%turtY[5Xr[G姰#+Uk:tȘ쒆<˸,1o Vjv{0X\"0r<^T#>xCl[R>/?AP`<®HF5țŭ8ٞ7@߆K $~t ~Vt$tPGHtH ÂZdv:3h {ޭ:WK|4[#u]699 qao =z?W!)55ePkۺk|]d{: \;ב콋D=vB[[ +$QhrcyYW%B_hOB*]:=DɌmIza^]X0A.vE㛾oq-9<؊`נDyԲH`Iwo_v0a+dlby Ee/OP䰯nQjIE! +>0ٜ+£pgp930mI]iDkڂTK܃ ߬X ٌ9/߆HmCC6uZb e nЁ,2)Lh5ϕu 7﯇gJ H.9oz6wut&:::}:wFOUGe)(Jg 5Hz_!0kM=<n]-$b6WԾO4{ w|_՛"u*A6h ;=ނOHBB%ԋ }C0LZ*4 J:h5`&gJҏ%*e f㕊$$*  A'u!Ju!|Eyú>*Rlx,s`!Z{;ƦvƲ ]K9ȺApQ](OA o#]:P 5ss;n==y;(Ϸ(Pm}F+>y\yO>r]ɮ6 T%dWwnڵ*ߊj 34xFgn)O*/Ë=#Ͳ9ʤ^Ų]TV*)oV8G)oq@?&@[?/bf8 = ~sit~ v׾2.2=d]SKlf1Ҿ2ٷd~)-G$Z 4`9jN&XKش4P?ʉ=f^+՘@v"!l} yPKj!!|͐+^LNnU!rі"Tۚ~ua/[H::CHK6Î:+g:d*g{`E'b8P_U!(kW*PmTc%&'#)w5W {5eP͑+,ٰ̐˞ kd}<`Qă+D8A!@>Ol&츠)s{ZޜqJ{\o!"/< 34߃1OL0v7׻*7@0a7 Ӑy'{,zn >[/Ji3cB`#_p/z?`1`sӊsuhbɔ1^1?N,LH1Gx[XL7pΣ}o1X; IgazjAH&#T8՘t]TY&uq/?f<ޥV "A~s*F)sç9+g%BM~:~IcURny&U)^|m-9"n"wmRq +Un899ċ8EŹ 2Y9 Tq{9ԉ`iЫ3P4a0vt]?Kl\Ok(HIJ۸m/s5Җù㌊{`|n3WS=vgha쭩/WH؆X6  ˴ )3eRy -=Te䵍7OP*=|ÜğT)Zz%  G,*ó H7"S6px:N>,?Z'&"%RӥL3(:4'JZ$\&4CIo-tfTm{#KG;qSi>ֿ̧wr~,  HrvQW'kQ&h? ^<\b\ѺhP+U!'s\CoN-UjؤYPZs{ {۠M^Qj `mTfV߈[RB=yc(sk/\"HyKFp @7c}ߺPiWКl"z g a7csꊬ^:VY,,9|C:z9xhcat-BݺyArAzۊ[9Y:ɜk-_;a>2r\}Pq4gr6bozI$\a)d߿Pz=NO9烥8n2/VU$^;H=6s'oK/Bm7G!9B>+D'BT,Y=Y485dܸ1u.Oy\[W|ӕYwʄ)%%t RCGKF$Eُz{ERy!&80 mGJ #J7=h3qe-R8G+ ՍfKu1(p &MLYHQB+,$x<,F"t\2?1L2~x8b݅xQ> >6Pyx9Ib˝Nx'Jc>ZV[LpmuO]EJ(bK_tm>} Z>B.& 3)VP FLBՇclir@mDƇDkQ|Kpig$A2[|{]z{j36A|+|4+u3y'mk*}Q2tj}?dhX_R?ԭlUPy\E 5aFPK4CR# 뒋 _]x\nP-8و#20# A"oK>&&llv嘏߯x7]z\r^Owz @->)ӻکЪ Gq[\Z>l&j|ĕ aP:lY]T4 nU7WQԤئ^Vn\xii@ܷ< gw:'{BmwycV\ް70N<0CEYW}863%(Z0""λɊc= ~&PK,Wb&oR2Mo$حTՀC7.8 A@D?kFQ¢qs\=.M@IFL _B$bQ悑ES]Po/*3֝HH: $dSf,C'H &6+t-OMfhv?D~f3mO@)N(A#ӌVKFF 6 -{)S(F/^U"/#nQ."*[tw ]M\ovj\`^tgtP>.TT/<Ђ = %.k\O&M"MlфtVO'|4Al1+Y(#|K8r-y2>댽8s]P:FGzhZv,M]̴!lKf.guU9JCKyfө{  B{sQ˝{Jkq2Fn]?5K:e\U~VB3:T!VF ƒDkYyP5\H=Y+ opnBnLSM|pYT駇f98Drj'E, 6+Ȑb6ՈU`('ʠޖS7Qb͕[cf\z/ #s&RqDaXr@5_]Gz7Vz;p'JvFc{h.C/ KpߏQ)P%: 27c免;B%ۏC:¸! ƔT{xڎu4< {O񄩉;@h8Dw ?y3ǸCZk_OUMJM+u_yM+űO\]d+mLg!6Mx^C\ bI ./܀ɼJ#'HdwfJ!K=I5ߐ• %a#GH*M&r0U';B\䜕-Sp3OWΤ ;5Y:.W.jG1[\$n49ADwp]Y777/Ο !P[A*/Kl=_r=}Ss|oE׋[KkmRj9" _X> ? ifdjLODIOUl tqK+7s˷̏`&g[1pV\)Mu?6 Ok0b:窭=0`#o)AaU@& \2kΌE2} E3 Gßگ~Ԑ oRjhCVj!`Lä'/S3GȦ7,ȧ28>W)ݔ":?>XD E5]©~ZDu̽ẋT OQݏ#}#2yG ]UL3|>g~Mf \hPe(wr+rB<\Vqe?DC ڲWw3)2RIc&Xj]t|:+9HAk'L2yg#2-4['5GP?DS_K}`5t N3 UBCZ5ZFxA5Ҷ,C#[l`6.yh8- Sྖ;bPvt u[8c*e"s19ךv҄,xU5) #DQ]g42(R] zr&0l:s{%nhO?ܬOoLi:ieMH)ِ*l+Ag_3%C~+s2vnpBPrW8j]yBxcŢ8eS*2 `?O$R68[t0MJwE->gQ D좂3ե4̬uQZV63@$93z%CYk?gڳcN$LVCRy]6]f>{z!vAљaj_5Z]sDMw `|F:gڎPUV1eْ|P̲Lyc[<z|B}w 0 TSL'iƄٝWBU50^lgr)*O>*j\ւ֢ w΅wTȮ&˪>dE*.hVqÞ|S 7/m 8.f6V, _PPv]H!ا&fw:P,,æKEbF! Xk6lҪwAK-`0rqq N8vϨњ(/k7YgsL*o½(3#YS#˙0`QH`W, "ګ72>Ҍ 3QWG*)>*5"TXT5#R9pC~L/OX_9Ȫ饍E9p8Ra=2+b*s 2xbҘgOLHXIJhJiH]9-G<U ʨar`n חa{l(D2Ԙ;SR칩2-xºk}g\dێ,J^uj$L$ C+$w/]8Xۃl83-֜q5s ބ^Zc$4d8NI%2Z\Iޭݘ7-~q##ƍ*%qpd9ݭ(H\(nfkշӌAKzjmv3i?It)\rzGߑY%==Ewx+?9isS!sMs",+&DPl,5n&SdA)iH@(ה]pZVgffhK$Y_ L,Q(ww.0{ y+?,1PP)UDv'9' ) ڶq$``v*vzG̜ Zn(i66Zm[T6|$B05_$D vgxw*Oɣ+ҋVWۈc09m$ `ی*g``l~Nt]g% w)Q/HZXsқP؇S3 a^ppz_b#0]G: I}H4 k QH2fJ4>6I4{/tEo7tmGRfUNMM>4&q0mPyʸ͘-zTYCWY ~q7[,~wKS&5xXaqxgϻuF%}lyo|"|-[-@𡂄^ ጸrK6^eWT碯P 67)}d@ -\ _+hL9F%ZdR\rS&)--R%JVx'JdNE?Qq rxI &^{D~t~67\1Q É_R(#SR7gk/˷VoGè/`/ m«Ggې YLO5,͎]8:)8fhB݋Hrci܅oZhE4N0DOqn:bpkc ҳT;g~OA2e8OԪ#XWOmfpEzJOyx?yrg2V h_}4t죱7>Rtvp4sv2sw_M]GQgMOi%1ui ˝PA8i[é>ߠ}f6S@6yF# 빬vt쑑96fE3?H#T~Gjͺ4rn~>,OR"2t^> F5b, SU8\4U2i Ma8b2  mIVIFps} pPs?Ky>M*7'f.;8K⥗#Xt>p&W_L~x,GVvV(-p&$X@4Jx=%j՘dzٌ.CwhfZΒY3Y*BfSF^0TjIxp^ȹ{ A/[TcDMN#ݵOP^Aɀ?bSr3W0D͹y{bN~2XÉMD$f`.0Y\˞ҢkOڸ_]ͧٶryW5AQm⛲GF235u"Ual<#Ƕb-MxQlbY+Rfؘi2ED yLT(I],p8|W)TBN{Ǧ@?z-n#* G B˱绸Yr :NZ{L-xe伓8@v^1}'U} fw^Mɏ1_GlR4Ԧi^׵_k6a/ cI—?~CDMqFw^i<<Îq1_Rn싕IӨ|@o/~<3 .~G@E_ ٖ(lNREHų|^ g/z' Ep9A3$S6(=pE$(2,GV_9a҇(Pԋ}GB=ۧmZU +Kgf+QW[pMĐ*Dy八gccSlN q=T 䦢~H8"G8ɪysONUk}s]6'[آ۪,Ye)v~+~>eqA8F$y zA;ȹ`3`KLWh[5B 5+?uOX[lO]Us?=阭4KkEU75)Y^ruKaKFM61[geO^H]ȪLtjU@+!/!]jy.=~z%y1$A:# S.2) ɉ>6cwՂԬX^ X{5"JTхV hl)>m{E]xez=vBQ6 fڮ8jdw Է{!mܵaVZ+#,3]rޑ60Ť6c5 &nS^2kWj֜e52m}:׮*fG.m.q9V)uHnYfm"*Ӥ AZt.1ifIa$1 v[e#2oU{wO:Ņ o[++DŽ9T n&l4݋ǼDXbOiU^/ qu9=/x`+ĔA)n5uMb(n]Åb"Z*U,}XV=$LkA2[yט%F;:2t]ZvCvefs||S̞UpĂ)/\wM =5I1Ҿq_Kmۀ Tgʂrk퐪t ŪVJ夤%X!25k`ʩd!.ɨad͡BfZq5ڐߓh)g>%Q#7yƝBNku0I|ڴb,- -,1nSˤ*y0\5+ER&GN^Vj"n'cμE5݁:Y{a|V*Gόmb{^)fE+jBT8' p)w;8Eh |Ɉ/o[q1>kpt6ސje! c/&X@fC#mpic-.r;bЈ2`B7}GlĜLOY-L.uUCڐ5C/fS{ire:${,ou2y;|#꺡R;>T݃dqژCMC 1fЏ1ߐӂ<φ[]Pbɡ_ςBe.ѝh(NCC(O)~"SҪ U^!n?{t侬FjSq\ abw 4Ov\osldhBU|[1PB#Nx1F(V4r~N΅k;"Wݘ49JiEg$ey5R+baj5>binyjXT2ٽwҗT{D3du%*qy},V]EZ1ª" (Bl%IU&(Z/R CXtz~Y̕X|k#?ǯo}:N GcɊOkܷ4+{BZQꡫrjG|{q;\괒KW.VT-VH Y+iC]g)ezm'T2 ]_0i$!0|ʣ n S>+ s~F*)eh~fj51Ӽ] :w)ab-+vR 3W+Ҩ1m0INGfog̨ v$VGip?ܜ$)*(7h"nۼgϭI\uw ǬC;6{kڏG13=/)Q~XeCΧ?&SG ɚ  A!Uä"sI**g+(>%Fp@I+Γ5- .A T@K i^^S&in@a׳"wE<` `[[2יBEߙJЦ6f2nz#[;\8Z[6z&![~U'o'^u g'N~ds6"AQnFOn*Jmq)unYbzP8p>|H*PZ00P3Pq&2c%aP#ms$m]Xl+U#y%*n?9se RbfڡE?/fg~<'Nt'M07S-e&G(-1ݡ xĺC~T~;.Ė*ҲW\^2a.p;sN`\ή̺)gA+<ßV:DX~?YT!Qc{w!l#]Ê5PSUv ɳ2C Q٣a/jRL_ "X]Fd>9:5݆u,>h(-P0&nOfz@Z]!AX )HL9&)xZߎFZ8فi!U8tVZ-֨*Teӯsf×Vmf F,\k E~%:ޛ.GMmfY|8$ 7Qi8fGw#kMV-ŃǕ\mu-EZ0]V_FpLQ9 7u"6*1`+W[d3S.CyYO)+C+z8%4;G԰K!g#ve 0`NF})ߪuE|NE,M-* XnjcǿW\nL2񭏮$YUZ0Y<Ԙ.&RlX)hM>J Łx*fogvD]2,N(`5O__ga;_c@X6q)ur=Tl^YK[[ɚ1*l![R-JӤs3?jhbTdZ eQ̍X> !QQ,{pޣy|+*ع*zÄsNw| smaQ>c8O.oܪee ?Y#0DEtJhqY.^{Qnwgy~W.e s26\fPVZa^$S 'UDBͯ1ePpb ġ*~aǁ?qX64S>.Q-3|=[ .P(.l1 ]g`䷣@C2/0j?k:x 1?,C)S9#+Г69 a\cHDYd  J!DLgMN.an֙wH6)2 i﷪uН;*0(?Lt;-i)pqc_w'vƑ:۪i>˳Qez⟡\]h9e6LzSvC޿-YRrfeppF,Q==%g7 YKL&R"bOpltp˨l,F'^RtYSXfbC ӿ9:. + a'jDk[6b>b]bA7H ƍYFw z{>Y ZN ("o.n=YЂdުRt]irzfܣRC$Dx4:!64P&&cov`WOL|+t;G*" r82<0u[cgC]7rضAa h3K[67JUg,jT!"YJ ][!!elsS$[دRj&CG֎8R T!B!IE{O}~z^Q5hS Jg <+咴8 ~.&8ӳJӃ.v iv{V_g1Vee Lέ^8hԝMn'"y,h1,kƶLƃc's]]}Ym&{bm!;(o LO/YţQ2aj7VZTp+L0 l#G216b58JBBK;4 $yM6 }i<+M+Jܠ N94t^Bl0?87knV܏@vH~Kb8Г@'Zfm!C] =f gm~E.v6Z젘fJas)b1q:Yt=AvI] 6Hb,]=SM" &3nC^2߳n9iPLL!⍼a]9\D8g9gVm4Z3$05PmG9Sjk#ZJKڑ(Ū[]Ra~mfa(tQT$W%X?r umKmͩ]|G0_齕b#O^uSCjnav9md6/[\ Лr4-kCuWD#L׍W\7I!b w~LHvD,(w>o vyaOwOXD.Z5;R}d$G@-j`YZqR{04DHa[PY:1Չ☠Uduϵy+q<o[UOGTK6IWQ9L#SPlߔ[FʜvG: j R _uhGe9'Ņ1vUL# d/{.PCBu)wݕ1\AGu=q05-3 G|HYвO $`/:eT"({MW4ѿE{UkmLHlSx"JlB/>\aR_~M{ddl~G{gʓu۸; Lj}#@b /8**/ - 9+ F@#8>3΁~B`5)&_W.Rqz`,q)NҮރx]Ƌ/kx' e=ZBV8<1WǠp$D3S˘vC ׂb>.mZp9&fNмC7VFAj3 FpbY_viA[so/׿քQgP>R? yOrV=(]e0)rJ_͊eOyv֊[@hb ,Pڲ5SorNg4c(spQgu0! m\Zg7"IȝfpvM/>c Rn?o'ޛ6 6~>{7jUIOV0k'{.L!MClE CfA'<7?q*7?Y+gcVMu_ahx3E. |/]_ dhPApRswSɅݡNW>0x8/s 8I56]t&Om, D@#rLZ6 %< onR}R: aߺ_R# I??j'/soyCoBhIlf oɷaiz(>:?c6^U|S*ru:7.3v\{ybl8Y\?(o2b-xڙ?Q,&oﱭ5LHDQQKgz%&A:]Oˣr@.\R"40ihv|:H}Tm#]&鼸lxt cGQe ۸#wxb5:zޙ=0SZ1MYnL;E[BѿTU`wxYO,:՚y.+m5efשBnh j4{(e.`:V,Gx0(_lB. cQ}ENB^N&V{E&83w%DM1w0L BRWl]R>Wb@HEDOL&)$V Lۊz>z¬KquSӁ7ǓnۚϸA5= < q#Ԝ=ѱOf\V7\gc?}d<,J݁%qnC0hĉg;C" %Y!?m8oΙ3Q-ǝp Rc7w36sGzOUȃ1!,>B@FXJ,2شGP"#e1O~sr/s FtoV!Z|hof{y噉X-0Mí 39FNLTBa_,8b6cEM*s?oYD+$?IҊK5H5VkPMR;3yru*sp aԚΎE0#8(@"˧A@{b6 tsgDO=<$\%|5Uŵ4hUcOv7oc$(wd+ ]s IG pCwHeaYN1%햜 E"Q ~o|U)μykb@Kdu8= =52Y,p&g-N#7BO=kݬwVJYXe{ W"Y9s>J06M͕TI!_t(Kkv7 @^!Zf't^Q率<%>i\(N UmE>s8 7>V=NK!d3]&`ˊorQNqQ&&B鉷T ljf- m_MÓ6ߓ_J>P_4Wr]iy.%}osLyy J`"E'O qL6>bL65MP0ZrlY6P eXR]bsr]mI9d;mRVeùg`%CMUf %8ccf#Sqs^{Xߜ:(omqxP`3 `ٜ;J^XITLeݒsz_vD(P'4'i#iF ]5)j5bdiKvL u`hex#MUEY&*x s[Gb4&y1Ӽil@n,ii¿zHnݷ\1j:^Ya6ׯȱF dU849SUe%JgQb!X{g/E ]F,~!dZ 1 ` ֮-NPtҋKI3V>yןو0w,Xgس~@HKPΕjM7_HvB9[/2ڬ||z+uWΏEйXRWު5UG#t+j,]Z[z5^_r=gj' Rc%=hO9I\{\ߚ?/7̩'9_JP%\&Eܪ_ Q@-l*6d8{Xj oxV<,wsȑugghbcO@$a, Ȥ%6nN,#?/KhR'ɗwH]ڳ&𳷢Ǘm8V/g.^.Ps7Vsg3c߽&'9׽ɱnx>h ,k7 K$A''+*(G?1p"' 4FNYm6=ԿN+(JђGa h} # #OԞX?9=D6W)5dzeyjPduu-igi9JiO_&044Js[7]\"w u= 8Y|# *^ѯfq:LyrJZo魑\fq++>{{rw\ qe@u౬Ӓ*([8㶍PO+ F-DD1}R'E. ទ?΁#\;9QT4<> ׌UֽkP0iT:w;y=| z_/=``nr`[?Ztչ>xz :#2 :fOf~JGh;7Rp[*ZL٢byeUL̔u+ )NJOjiZS}lmP<'L(u5?=.BI&Gbf8!<0Rf~0 ߢ.|0ar?r?,@:3fNMbL ›$eL{?,Ƈ~d쫁Ym 1LxxF /("J*NR_ /DƜTnt6 g)b_@ g)┐9N@eGL;2m%#4%͞JUd\_c#k^A%Pg}՞BwG螢6H4D=C: ~m(}qvFBw.laxSOEGl1SxHȏs_DwG{v_/HIsAc3[CQWƌ]tWku=(GD)4qFc AKu$SF BHPW@,D]!7@CHա4ufC*^.Uu,ْӖԅh?111S4paDE%Ĕwa*mz.5*؅M^i*q4RM~Myl"qיUD^¦?r)AF,2[CNo 0v+J :]MiA ]m;ZM5Y#Z#7YG4֗GCEBML~IJnr&2l-O I}R8i/#Ym&#Smv/Ը֏9RJ6f# M]篻Zc ToS߾1G!=^O^Lއ2خC[=f9慕NU , ܢ}%Rk!lrݛ8Q(?>Ul@t3[_fy~q^0P2]3D;S|6Jx:B7iuHinx"M?i?h\Nҍn]- >J:/d$-uw'04ϋ}x$}ęi{F{o5rʍT|9|8vs t+hJ7ץh[v;QUT/8<*"y+B0fTχSTV<6 EA17"~ (H[liMKKI×+q(DU%H=S&VyV5ؤyEiX%/XBkV G݄l "^{I^FP!u1{fl!ncg %(+)܉U|& jG(mNH3eݺRYp?QI N srIU5D$x$ha8DzS? 5ߐ TR! eݒqt%^ ?oTb4XΙpbL׃R"Eb´KĎrrbftMֶg2!֚O!Co sc$q"jokɫ)Sw"8IFi5xGgVbI%!|-W2ea`T@QjuR!R[Ч'uCg3VS蛤~YW}"{)yݜ6VCF<2We#B{d&2$/ǯߑ ";𰜯 ?0pKoIw?XH6_]QD,ŻaM^| gZ ?<L-)KྲgPUg~n5@rmG4*8c:g;%o_Hw-;EoWHv 84;e #oWDvH:oPUt0} ٶlI"IctgcA IdT˓ƙQghX?QCdm ֭xBTkiN By[ Lu u$"9R)hϒiM.@zIFTrn㵘؈{jL/<9w?J8iJ<`n;5DOʤ?4  ^UCfrҏb:*ŏHgb'S]MQ FTc ae$ ԫOCu1a/ =XvK'(γ/p?$+WܻݣPOQЎVR'ahWXHq[i?Q8hԈ-!PJMacz _,EWze}k@aY;R"J#i&=BV(kjH (OaɼYsnZ4|$BaWhZMTN @!Ow$?ꔲL返,) \5.o9:9\i\xImmaya9*0cPF}*\*\I\|`͖׉oԌF/wD 4ɇTkҍڜWKIcU aTy Nhi;Jqg2I3 ڞ3'E5+BW$+ M*qrwR#{KY x V@i&`1K`U ʸ2\V\': s!f$}~r4;Zش۶'/NWҝcR;o+9Jd4A;p[R&kDRFS "Q|gӺwRkiUGdD,: X 8;/3,%ah H>..ɄqDEn 'Bu)~ ,EΌu̎.tԒ!t[s! lo\Y$&:RNaܔøfХ啎âSy~ag=F1uD\D47>lmnmss[|_55}U=/._f mE"gשߺw9ȍ*aUi8jk{ߢs%XjKjZQK4gdhss1,Mz$E|fq"wZ@4ִ˛UǍV^aC"Ѕ`^ F^n-/Lс?.9/wI-K#3u+HUUyUYEYFJ"-x)zo[MѢQGN_"'q0 *::-fd<7SHõo%eW{7i< ;Q5, R'-ϥ-40*;u uyZm`B*xwf 0 ;Ϫԉt4cSeLd0dĴW6,'R.gÇ&F B&AF@#P"q`Rh۴@6 !Wt Zr34peF 291gk bK'N[d t=F Na/}\?*,o5N dO{ژ baF u81-=ضl]M-2C,N0-vz{|]" WBx+ҕ ?P.rs4H8 Sv-MJ% 4]A {FO)?}.\x"1 KŇB6!7x9vY Ka~&$B|~h SJ"">S̘>0 |_ω$*-Tq"X^ r4|roykXa fI8Z@ ,eӛ^NHg:1IwYbǣ5&B!؍%D28n:N²=å2fIi*Jz/6D?^ȱr.|Ɍ T}_j-h8۷OֆqH*DBNV#[![+ \\14ꕍA4_x Q;EMz- W%ws 8k_Hײͦ;\<|,ޣ|@ՆBƽB}Bϼ65C[\DV/;mٷ.fcC&gn_l&PYvPOz 2KQNKL6OCkei6V+m.<k'd0ƒ4k1oV,Te2mPV4^8h#ɞTNK":bTPIAѠI1Ax\kYAxؒ% B~/`)/o=qlե.cr.*mggJiv@:Wܭg2jGYK>ZUՅ(bU[vΩF׋U7?*2^:=Uj}4y?!(^@Z`}Z7NwOP;dJ]2\ %YX@fB!)!'\1-NN揇ݣ@Ж=<{2_B܁0Dž){D8)fhT5Z d&(XMmQ0JYvW6<ژ%a~ą 7hPk/~{-to)Ŗh,F,|xu&=}V}Fr?^~T"Qm$l @Dv }bN?p|%meyK)OZu#??OⲜG*Y[K·It!aF`zXHQ_Y϶JН.Vp)(bdz#<\bҢF‡R.BocSi8Ƨ 2r`؉4"P.45}JadU޲~/km("w'ocWRksdzMg}^d#hkݳy-0u4.kͶqMמK0lC+Ȣ뮵ҏdmmH6T*t2ʢ}6aq>:UNZA(#Ξff-}#Kp أrcq6D"d=|؉vs͘Jq#CtnVPa0ShdZ cc">I0HNEF ;vi`4S72w "@#t<$"\p73\͒JmUy couƟ}VdO6>^*#xt3 Kr/w*|OY? /J삚 O$S\2|;I6#õ=I?ʾ1:fݲc۶ӱm۶mc۶ӱoƶm[wYkΝSګ]klaT G\#AΠW-'rȾ*Ik?hpW0VxwS-1ظ?7+ ]3ZbʩH1bjZe␠_b0Qp<ƕnȬt6Pͨ,D`Up_ Q2%0>Al RXDN8JeFf,q1(v:5I(J,Q07N9#QhKCn(Lh}cm\gs_>!1O#T&"]pPi;@ܒY#1Zi c4yrN],x05$-6R7F!~@`Yİ0kPD~'{6oJj}r[^҂ITU8[LmR#IEF4ߞZ%%5Ck:W$,4t$x]c]wuSЊo/8DqraχOV|_,ZuY KlKuf 5elܳIEI E^7۩"@:' i CuϤJ%v]>37|R٩E{bwdV ȷ>)N]&qSlo85va 0X 82qH0c# _,~-_L:0f(z]f.A 7f:\yӓFoLPxuDNcN>C-=yaXn= 8d̒Mބ?gu{hhG Rl$n]!i|j0C@5pd].0fػB0p0:"4r*JP?fy$?bEXRA8nW,J Hjݥ+uԂWt E8lQ6IMBʲ_mEauq津8#Պx;?kRlNKWf"O4Y<` ۤ =a5qVi>GOm6;p/I0qV0ViͰW 0X.Q0.!+a!1UH6SEi.}‚$,Ùʔ KWLjNV8%D|ʇ dW-зS.҈B$!baQyvcp=5 =|/:F"nƷ ;'Cv tl'ǎJryڍW07g0%-o!~A|Y2+׾Ű~_.ajTf,Ja}mӫm8w MѰMg;M==M0>sx^8X/̼$hL# n%S*0 m.%]c:nd-6UO1"2hH]ӌ2/E^ܴlݓ28QX>9;a]& xQg,ϔߡO\cQz{u;T ɪA] D|r ?erJ bAhq}2[&~ Qm$"d=tcjKzt>"Kh`r&Yr6 b46eOhfLB;RahTo;(n2!#OGLK%.O+_;ӂw`'7NK.t,6E9&d"UDnɗ n֬Yk-^QNQ$¤d"AIja,5Q%U8T KG-|^䲖Nt-/NX3F,GMcl|kUBMnITCNe'5hMl>f'k=չ#v|1vWXϮTIFG)B`d0,wM{}esnλ&K,]+MnGUjBbuyj>6F7F/KA̋J`ԚƋՑPN}W;9E#q<^`-،+8 Y6ȚR(y(:p(5 >CտΈ樁֓ٗ' v UX% D Ү Xm l2d%3\޵iù}UuCK.աSbL ?ſ/KLUx/=.?Զ$p4^U ZhVyPpe* a༌1kCp9uuj:u+`h2!Խ;I6 ?mTouރg]w=&num: CuA4зxj Q)*45 m&Pz c"hԢ[uSl~EF xi Y EfT{w]skpJqOIu08\R$S,KgTт+2E݋Qھ|U+Ӊms̖o8\`8J~[xe:6) zPz5+uCjEuw:/ e#*r4C}{ |?Qkxzpv@*(0|x"Ԕ5^K[F0P:`;`[uRIzmmKY0DG:م&Z, {:\w%xz`eQ S<]v@~#r&:L9 +SyVLMj6"A~"yzp>%qV%˰~L 'F~Mm?/Y=vD~LriUΨ̝-#Y%s5[I~UUKicW|Prk jPHIswKrK*_,'9| 3tM 7\y*D# r*,ouCxXM?H Ɇd FR2 =7rNR,KafK[|UԝT[zH @;*ܙ\Z`ro>@D'_H@8R'4᥇h8ނ,n QId{N2L@0:#ͨ@k =B='ZJm8 W;y@ 9s^R% _ol,d˰CzR"IH<Kq7)('P ;8@!>&:3{b ڈI<,4/^O }dfOYm\.Py3;&sf8 NMѣt.#0u?o~rC6L-oiW_d֒"Ѕ,lnhr>О1e'lvEߜtrkRCRš U;-kZA4);-t^{v^2ފ^ABʠyk+[oosԀE¼P!)XaqqdWdYmm)?kC G,)Ѽ刧jАJ]+MWMqbGV\ $^k33x<?d\ DˈڭXƁzPef4Ďƽ-Aj^"ٜ񁹎}\hKx)v^8ZClk{%Y{&a >2Er!p*jC>g,M4OU)fRWKUQlv0Nl+sSCBQg"y8Ÿ抙E8?y?݈6S͹PkY.iGV_SjŎ/dplcC@=crp\] Hf8!bP29"bR:Cƣ 9f2KDc UoӏqiG8ҧ揄/2U;&Y؍ЧhYv7Q)/e`KmiD9 )oqH. 7hUu푛=&1PE!]sj'iT} C нCw;i׹e"N 0p$5R"ad4 >`bRw\@4o`%udBz{a@֋.x搥 [FC翓XW&U3Z~jOi)Yc$cB\|ɷ,B"$,"r"𜬜H)0!W[u%sy56%6t$$rJ߫K}481QThϦ5"+zN7eFhȴ/ɹ ˵.ҾjD3`{z営tg3Bs95t%?cǤJ:=$^ǃ{&4Iؕ(QM0vAasf׽YUA}Yv>To=H[c"amx|C=kLkδv&]{9_BZD8&7OTC7><ڑ^y#YeJUx0O=d-oZs&l'":ǁ3g68cz`AkΦiclB3/r;/]\UE< kR1q]c2 +V;íRͱFLN7XRXF}sQZ zBx#6Qtc\f ࣖVtoc58:Ugsyeejw-leymJqD0TZ4qt*ьi .\KNjAvmJ$Gܯmtjʝˮ7E-j>{8Bk>5z09)o"$ #Qf}j٨HBmEpoGP\+)iEϔ*/r݂Iaed,t2IY}UEA3-F }-}AAuj3Z8AfߍЃߎeB=~Ր LyàɅ$%꜎N,',ؓ Ҕo|{T7q,]t>D KnKrG׃dI98j(Uv߻NQBsbߍϔ1)Wx~DUwCcmL5͸bђot%谐G'PʯT'$qP'FADl̈́xBg)tZi~11 ZwN+e2PX̥4v]"kғE*w,yk6mUSFO| ӿˆyBAi௩=g.dH?J(Lg^ѸoIoP:z=WƔ3=4N b<:3^u_'>` `qB%uh * `طLTJ"#&V2Ԟ/f9.}PfYG"VE4-h p۳ܩ!!@H%9cONKL%րiŲo#Pz%q2ẹon#f6&O ؇°ιvX=<򧆢ɥ 9z_+*ݭ1+27=*^?,(rJzȤ1H6w&8X'4h |5:V:ߤ?9Q%C<1X2oh&'P`#`rz.C[6kYҌ;1oˈ / aUgM rɳM9jhž#(z|Z9M.-L*n.qѹEɩQ;8ƈhF]E90VJ_/Jnl/HHJ  ${?8z|Ω9U1V0)9X%9"3dr"$&Xa?`ޑٱ;lOZ:D.׏:4A9$ | 9%ij{7Z<ԯ7o8z0_;]7G}qڵl,_A%5lC oȾ]")[| -Ւd`kifYjXKqxM烱yL<|a,FPyoQ[ ނ3ϘkYS}9G c5WON6v;$2F9cEƭ˫*. F_0}#G;0J>jJJNh V:(S$oܨ( R-首lKԤ TXϹU0p$.[ӏ}p+1 bܩlB:^'/٭N5y)0ɍeRS+_xQj.)Pv5hL=,$ȍ%Q"|0ămۈ~=٭NIKI`D@%nm#syIG7޽N@ "J&:ͪNu@\o@({ \: ɸ`,ƈLT`8jݬ^L^ADP֪nbתNqf]Yw2d/UN0NG׽?' & 6Ț){+}B)Ix{[ }4rS8^å1eO.hQeŠUnz{RVRq6F3D8!6=B$P3l_JYOV|-͔Vpt"sR=S-maܸpr8reS2_r/q&}&w5 lCw9:榌-~ϸjQ6Fʚ`Iz_h%ūr*\ciKJjs({K#uj4iwzΥf݅i;1MJr15 L̠7KN[s>q^($|DfMޔfuUAӼȀKrfI2BWۻ,);CnAM|gbV6~7]-//kWt2,npP9\ d9HhyŌt!m_0:́m%3hn *0,hO9' ZB8:- e~'*%T= gV>+*(- qQmKuOu0u8ΊZn#BN׼^Gcb]Eexڗ(XE>`~UR`1"5c`5?ɹ K?0hfԹ9/ھvKx,"! Ƒ 7:K9]\)rDIVr8DuKkمv~ytUsfo6&-@LaOZk45J :]vaGx5 [J{42"I BgXszn/5M˾yoGRNcNo 7Os=t=̳+Fbi`6/ٌ;m%|q Z&܁p0Tw٣̃&-X6ΙgbnTm;!iP<'ldWga(hFWcƄCGTjLegCsL$cQ=t*MO(cN K3a%(]֦_:UgE5Ŷ$c"3g݅LSƓq$Iaُ-%Qmrs E+^Bp+%8!OX鎨%+ 5nTni?ogl8SJs-]jZcN /;BB1Sƕph:):Rr)U#^|L#?cL^ X#e;RΓ\h@H wws *Ni% ƣ;b!(e?/?ŌӅߺӬ,DmvO$IGc'1CۈS΢^X7lt-% IS(@[4ʘNWLZobFWlޒZ3QkR)S2:kO#88Gaśhvq,iI#{Ѱ̹^Oxjxվ>dW̉]Rt睻:ebXm0qG`K$ge B\%gr])nuѾ>yB :wk]PX4@vV%i#qBNƊ5 3ZʜCZZ-q_W{:xaKs82<<vc0Ad(&RwbFK$+~;16z ȧYTY|xa#؍Oo7jܗv5D<o|Bfܞ45;Rhԯ!,HsnBI~J8*wO} hj̖ u^ KcQHz 43|wH'@3 KQ363 g"a[)ӪhuZ]YH]. b?/<ަk/)4ޭ9~W)K/O淮[_ :3ߛHNcEaTm1>ZJi:%A-)fr0U7ѼnQNW‘PL%4UYj7O(/V.ٔRt##S%ReU!xv>(fd8Z$0Yπ߭JeDPB?:6eLvNA#>a]Qz|hcYDQ'2 R!<^"Tk0D'-Y~t!ev8NI= "*'tx<%kLZ5{>4ӫ (L<ܫ(VIDzGT&/pEL֢,y3宂5eFD=/ppzz͢Xiz*IEZ~w4h\75zoFxs@2=?xPs"з{?^]`+nXtXB5 h ;R Rh3_yUwB.Vgxqy*M@)cǞ_&? S9JPU)*תQ( ,L&TY)|dsT{PV߅uYk]K 8=@vC+fWPt&| )53$'"Yd#78?DWQA7 AYjawr%& 13ȒE/ !"DaV]]ǏDcUeCnljgmVc?.!:9-B:-)/'h}xrpE9W$4oBx-UE7~>JA~; M * oBJ"4~^!VH"4lt4.$XY6nq)JpUɮy nGyt "WAj}0gz00y(DLB귤/K'9Hƽn4C F-뷧Aξ0&Stgh\ \q>!8#ޙ$!! 8D<8ĂQ;y!\Y_#V-IbuF1[0\^-ǏQW77Ec|]ݘߓ6c-Kb* IËyedqA֠]hثZY/Q/CPktw,jl/_ _4yDk"~|~<9~C"Ewea>gDSf\M^=+awa;N[#"k|L58gF7%oa͂<͖j:V#]%&' RB# ^9W sGfIЇ+/U ngb&%c#D5TƳdd4I siMSY[ȮaAWHO_M9w Xn^ bb# *:J-Ε!Z ɧ̃ިAWNН*bRKjĺRx]Cјa ?* C6h ͂v`nn6,ipOey}ToJ3=K|KnxBM4=-:Q0mKC=`bONۥ?֒#:4P_go7Qev-8giwwN8TGPOCu?;&r*b=Za:P:#KOۺjjRe?By P㳰"<6'Uy[@e@$ 0I)b˜CQjPĪ9ڷ/!G wGiP2er"ƂBol\MCT|ļ^ -KkX&Rap?2d]{r-dn:߄~uE$'Ubpxr_^ a02IyBʸEXDoY7g2xH!Ue&8 6-=);.v62+5 ƱT4wSw?K( iW!6S-7SIs0mOFNj/^UϿ#x[[&?lL~|&['TSrc0bXP @yRaà| h'w-3K ŠDٖ|F֦-nѿJNr2,$Z) |?&4 uLJ/jR6a;yT3ѵVT)/qT!i+Bqa1gMm)єٙ =r0-6v_q`b8p=# wZPet'bйyFMR:5?+<<Yk<25òuQIw*JEX1"lK: zl?<֣m 5l& 2.(CL]*Q-*:-b!,8Yj^ ھx -ưVhA&iBAv͌P.໑jjv1$[JАqwH|QO".rsgÁәQnjO. EXSD&֑Vcأ?ib'meE[Bj?xA%:KR/8 ?=6:' bΝ`)q#+\ WsAq ߮Oi\ԂGPqOj}e\si2zzY\zb#>P{v4btro+$\ǞubN##[\̆h5H1#co=Xi4/2zem6 W#8+ BJ^.sea^B$E:[3?ߒ+p#.PA֫ܲEm q!31@Ko^9 XVFo "z3b0wm ii_~,^dG0[8Tv'"^,u"4Y-m Uc逮3($ҽEl3$%6,u gzwH;D+ڐF}2Mov^Q|y2sJz >N\ a_ uQ{>w2VJPw%?0Q+\]1G0:M].$)ҫsI Ef@wpY?)5q(ۗHU}c4ZNHTe:Ư1Nig~vZ$T dkJօ,Nk0dEm&se=*UU7/O@} ,Jkޛɿ/8?V|AFܼ#x"OF%4YSH6 ;\\Zsf~O,܆4ZQ $)jpBl5{ 6dS/m} 4A2yMͲ_"Y Sχm (\=V14mb"=L~YMkJ/aC.Fvw;Z_6~nWq{Q[\[Rۿ{V |" Q)eT>,~4G[,MeQyI3q x60X 74 nF;ok;=06UFRQdjTޙ9Taq42+c}nWF>쐯0MFn8$#*[cy_:N£")]dONLG[7/R d_ J_^LG3V? ݰy:iF;W|dbs֎m֎m۶mĘ8{yunOݿzWJY 7XA} ѭk5boJ$g#:|@e@j z3+w3U;wQR&d#u'd;o^~55LF^¢|Rs&Smə8TSCiKgɐƗVć5R!xh.Hs~)VV%s > + N[K$P]Z{ /- >U$Oc֘!j6NCSsK5}m 6D7[yЅJCRJZ. 2ے|$,’у'Ι݁]_fG#i+&m,yy9LNw5{sZ(_m-uV_f}4 {ݙ MÈfgg)͆9M)-ޖA TȈWdCߌ.Mʅ6 T7Pr˼9MZ5Cugˁyy =[Xn<<9LLk7̶q|4F2j33Բ^ B*@%P T*AZP1Tkvs m;}bC)>9g ȢxKwwh8΃J<:Jj< Wt8uI#v$ JC$"(2vJC!x$j뎴a[0 mH"hvJC"iȅa0Sv aаv av 95ac39$vQCK!xy$-L p9T;Oop}Ogc_:Rmy_qWEn\C)J 2^K)y=^q'S;aHCZ6n(5FNN q TcVs #\zSC:+C[H;q$<\e0\q"؏ {[{NHqFϋi @og4D]+w㰝,N!}bp~\6n~Jv>1"f[~[PV _~fN4UlbyKOp0We,SdPD\FZ \|*me׽ׇ=*<9 uӳ@g!jeKM4~D{nSgqI4ݷd96 hK];M1# D@g;H*ZzܢK!$-88R<„|&aVۭ(ꍛqOh 9ihА~x88_'u!78bP _?"_n A#8 *C1DQ3-G 1W$"3}C^љIp}c~Ͼ eV-Cl;-NS$LzVEX9#M>$y ͱwbgɤZRk[3CLf6af[d]](_c(|h{0!-(o|C! Nwu`'~ Xh߀IXٚٛߨ씧dPS9Pk7DPCRWV[4H}EЃa@q a*#Nog ^_3 39T ᐗu' "&YaQ)2[Ֆ] Wx^$\Bcin[p.p&wK..1`ںzư;D6$*ԋ΀AM $s풨9 e˥^O2nA =(1(r,6ݔ3y8ĉdnq^Ǥa?$}88 c1x1,{p1@"{B K`uy &G6;TAߴ`~=%3ևo]6HDBҁ*x:u`FBg]L $R4RHh؈"9iMf úONخu͞Ͽ^뭵5՞=νirgk]oS}gRT{єvw,2%3r<o{u 6_ IXI&y;e2H(^Vj~{S[i3a0' 6V6Hb]f!F-g7HgFOj\@EI`vt1F A&vhqf^mK&ثjdt2C'Jb椈+VwB3m][ ԇIdG@Gj4&) ܢa4*)*ҢD٢Ey"5J["KLVN ;,8xfUQbb.s&G 8SRAmEԁ~k622[ 82kR-wr ^g܌"YBnjZ@m@7vĊ wlTPgrNTPp>7xI(,m=Q>rZa񶄤uO..5[ !Ic͑-S0HA;КW v ]^̀3BU70dL儁}` /]ڄqrm )؝NJv}xήz4j#g0"V0:M.mn(ɔ1^; ^q1ZĐX-HUdm-By8BFS,6Ġr]BͫDa`2` 06ImM:LםXhBd ]y39P "[:wꖰ6EJys]?hBaf+!cfCn?;{BG:V.t fi*NhB%Ds&!92A#7AS08`2K-ȩBR&LkGfZ" 6b#6RcJTI9DB>+J LHq!<,bzUٌoV^`>'oӓX"4Wraܠ}T.x" $:  F.EP SfT@f)7^ϱO#|s43S76/fLT]ΒAp<x!b* e=Enŵ9{@z/08~%Oҁ]p[79hǤs!!R)ü250 _wR<@oσo9(y{HtZfIG3xJV+8^0=tg7Cʚ-/nohV`We$mv~ >P}*:.s>R O]?F"hLdr6'/nF ʒǚ_a/x bL#p|ǷZlY(Vde^\2=4˦I?"Sc!jğ/S-̍'Nga9Uaq ;I-tГ]S eʼnR.L(nK_b 39qus"VOK'2;w&Ou; avpt@d>P꤄)a<ꏠab/VwGKYeHȊ&gR,Sm֣q:l:V9AO!Upxl.h Eu-w.XH#C{@qaȸ&r?Rf:3CF+&JzIs(JJϐљU4rc+۳o:YaכtV0Oh†w+eF7䔭a/P$q( ԢsvЇ(@pwY&=KVm^"gs;asxP>BƭVN%=4Jjǫ|Sۅߓf_&ݭ~T' qE\>kEɿY˞IluOd A^ 7 4{%]r Ưw9 Y;2n*`~PԆJ֍ٗ|:fq,[MΨXeշChE{G{픇)'΄0(0Z>85 v%+ "ǍaF6'LY3Nl 9] Qb},i`0G͒!̢1Z0~γ\>Q>Q V )ČOyZdP@zܥ|? R.JS7c!:cĿϧ-ݓh;%䗕-&h'YuDh@v% ,Jwk%Iv ek4sX6K^YiZX$.N.o .]^'YT>0uk{ q]@ B9+]M:')ŧ^.fʘlNdt; &mK 1rj7BNp|ˁ0wDOhhyֶT!۲$wFbyD.JC[ӖV3sawn۰ڰ*sKr H/{|Mix)"ղC^TXԻw_#   %(̌`:K a-K,BOkSh#F-~M)gCߒׂ,oWXGRsƃ%4}f}#NJM)sSßu#U8C@Pj Pt-RT+MϚ}:ݩO <lKpv\Sgf_;c+=&qf8=hƽ|K[)G%$vm+v]@[x1&ѾS[/!mQxt/&yȅ.cY+^[0^_!l,{vtTd<!o2>ˡz+Op1p$T]HjnV3j#1:5r39̂Wwʻ= ]9UU^<`Rʻ>7=⃴雳{{ O b/ʝrjH0W)(ҕ3zY=GNć*<[M#ixŜ%])\$RVY3+#oN/;݊x/^)uy%W xN.qUI{NvE9|4a#M+0#wxiW[G#(~qe/> –hB&ucNj[hNZs+[r!^xxVg3[?vAHi?iꟲ[A<}A9䉻9r: ?ᄴCG>h>T+_8c+pskjR!R%k7YE%:"=!ԍeaG-^Bx)|70iQQ{JGtszo)0?ʢK|n/)}n=@!IdP'},`\_9E`i^zalli r,yKG=T}6V7Ѯ0~5 6_b7~תUD8h%V vt-gl\s5e 2.c ǘ>w,ћܽ5]Jq4QMȏocH2Q$cKJ֪iy1s(=v)ucpխ~ *<4 ,Fi}))"OoZ-_A$_"nDcHFCǷMӈuic_`v#ӊc|\ML0?4[04f.в_h_H"9@r ͣ)u}ú׏𚃛m_+)|BCax(hfkUvFE%·"QsN쌘 MSR7⃎\Wy1ja3]6lA|NS8Zӕ9ׂάa ؽW56pe Z4IqlL^oeiˢϰAT5ӹ=~P&gB:&.]yDw樓sMm]OsUOsV!H'i}5E!$:HXvHiT y6R20(Gki%#"ú[503Z6"}PtVGl6} ǯYOJ=o^@hJ*e ŲcՆx[͉%+Ns%~q9.g:|}SE݂ɂ|o"5*x!2Pdj tؐvWvMYgn7Cb vUqVK Aqfț]8F)橀Ȯ1) 0#p[UIixfBq>e⥑{$z*3BƩieUlg[BI >?˙:ps#Ñ "S%bo2b8-%]=YXQEd/@o+YA 4rD'cr(OA·v /kZ3yt:5uMeFn1T-Vߠ CxG/=}c7ALJ1]Ի7Dž*d`vE^Y',oZZ6DU5Fsɠ ,t:T f`2oI@^ UZ9p[ S 8튰lYv-; :NȤ*%Psij'ftW {]1[lI( 8(9ݠi.NZ1)m`~gsT` 0Sy^`Κ_d؃bGVݡ+^[ p3U*?PɎKȔ!iZuF[Μ-2a9qyN9x+];^7/HښG݆\j53|+0勵#)~-l٪+6m9ieW*T[P\ȀX5QY(vo˚°qW9Ͻaι[%e(lm oRp;eEbzkly>> xatȱHaH*PEO'`4.4NqvQl[2lIKRRr,ǹڳ*XIes._ =} KD3sy.K 4pï#P/MQdsӃjU;Z:x>oQYI-Ke``j,q.Vn뢊YQ?ŭ#c|H8]GʏfVPVO{e0 e' w/=hά^C{ڴ5OI:>]FC=:d /\ng؆dViאὼ=#*k(ԽPOnKhwl_!9oa9OB7q*DT^Tvɫ*?8OjɭX&?M*5w^l˕'+N݋`M͙ڣwd)OW*p7p5sO4ib~lO)5R< Ĝ -VGtntW4c ?7o$eCC6p3}"|`#o{2/ry~iEA@??d,6R +w+$v-rR/qiE:K$QI9Zd{tfXAW9@ϩ3thN(%ŧy["P/&TF,Vur8fn=<[ƗTsyafs^3>w^9x7f??(d}ly7|f) _鱌YlEXiJX?~dSCTfqx%_J|5Ҝ̢qC ei@mGL kCWz 踜cև'_!= k?Em3OۯRGugZǹܦ^Mjyv`t>@))^~&3%t=ph50@gxFp0O~dSFk'E/pDl (I)9f0 [XaA臓x[ NWTmшV);^#Rt]0imeO]ɿF( u o(m}6e`mll>¼9 fP6g[gH~0NVr=c kj)`JW=nnoH]J_Y+UJz@")t# P_[4O;6V)CwE,o·t6*{pD P&\F&{ĞN&92tHm# ĥCcT }4'.ePMmG?k8HC";#4T%aOmRg8CUpOfyXx"Rz_t'N.Nhpa\ ukvd@u0Ndw #|k/2$;E_e.,ɬZusnv 1>6'&̏"raH))c4d!͑}JluE5 Q39>Kkijk=&]ojW?}SQF4rڻ?}0דCiqFy_i*haTEU\ߑ"qNM/_zd 5ߍ4ei_ O*;]]|C͐O)9nH,J*7w0 9 fj x:&qRTSSTmF>"ȭdSUhh\ᦠSUґ/y>+ qXa-`MAy#58X-;3vtun17(i٨eӰ  IJmc#{ѻsYUք=ń:.e'@El7/3uCMwhɅ4r'rT#JJIOi [۲>큽3^Qy0VIYh0H3utYB7;dNJa ;y!MYb))[3)7O'2bC1VʤBgdDK-+`F;U}Yʛ.l-O j@Uیˢ$P !o8DZ#z,ILkcNzB"s8m$y%GU׬\LLϘͷ.0Wc!)!INUw[wy./Ґ#MD.~BY<\C LfQW?E;8Q XHGhm5;}LFyn~`+* PX𴕁-v4l;lMڱD=XTCtgB<=kb>8 &3wj"]6G/}-iŶX#˕TnW"ءD&՚JNR 2¹g'yZ"d_Z1K "  ~g1sN0 X9mͶqY 3 <E6ǒѵ%R .&cuHcaS H0ޞ/Ђ_ %,0ħ@L@Xijaɑ'zd>\#Q)Gt~,7-|6 h-_EWǞ?_|Kk!dgVJe>zI.g6\ǘveE%z.) v(70.7CQ4o],8EWW J#bF@d٤YRN}Q-nijdL1Ů8.2tmtG Cx(ejj!dZr1wi<v=1%A^fұh2Fpp$V+DN q"k ."=Hkws?Q81nJT!S=}CM9n9=we=`,~99p!mZw0ߜhdǹoh6;$ DG`X'N`BۭMDZ4?$&"ѺŗazM*h{K[ jK97>|Ns|46L+TԖq`Mt=,!V<(!XlgeTQ,u bжWbsrS/J?Jm x*2&|_ X]+f*(/?B-lc"J(7gS NBP,n;V5gtx|E?'gfl:~MoB_ITj,\LJ8e sCnq)nVZiLDN1\6/U9zZD7qvS8qBȣC ᜁKvu!m8SCW{>5U#lr;"cF z\3h; +#a/̂lnZ 0dm/~Rb$ί'W @}AXv bTyUꕄ"na 񵜸(p=#Bco‰K݂ bbn>(FG–7u(¹}*˲7`i! BHItl*'/\MX[0o/ PN_ςVIE>ۚ|J~-B\OL cnIo^d[u0P&MZc%؜`yJϣaDvR. kIlUȖg.KG ؐk4V< ݘm]'_cQ: ds.5MV5_Z Ss K$"%~ۦQِ9e9{]Q[}\<93 L[l#zG^T7#2qMGGSe:5iӑ8H[rwC謔NUkWmvK&pB7L\㦦rX6tUjΉ<ĭsX]i*2t,QJIwr3z A=9:0)'y8mtm]3Y9MU/ffg_viY˜[)'ըkTdC)2T>pyLfT-?>ApQmΚL^298'Uٶ_WG})M3KZɩx)IdoNsm~!}A . mYնMLޔZZ`KDR&kS0ϴrR˺Ϗez.R@=o᧑[60Crur81lJ%= _~1NkZӶm۶m۶m۶oڶmO'}URH}wsHRb҉ģSؤT 'bcA*DRRt8/9{DA&SlVN467]8&8v 8:d:GfYBpۣRҫ\hϩh5#ҫ+ESϐ &wnF0;~GV~8ᩏ!c(rr{ Ȱ<ʡKebSsط;a-4{堝.z3CwW8 %lvpj-wDޤ P>f-U;gO3=_t,\TaVo[la7M(Yte;fuWSvt,ͮ=Kx^Ntv ,qo0(= NsYwvp/nԄYS&eo<_@)G|r~G+7? EPmԂ Q^1*W$dfoJO;s7s+ :N8m6z4o>4%"eVT/`| ~$XH d%|a#`H  gH*;\Բ/M-"Z@f , eSlB#7+v JTFHOmSrq]c." ZzNq+%}1$W댸%+?E|) dFӘ~F"Y,Dj|v|&܁XXʂO ʴQջ 7;ͳ4 m8O_Qၬ:`zh6뉎˟9Ћ VtF۶qDTwxZ;~1 TLY@je󴸄 F# #|<"B :XC?R<+B<a*ZtWKwd%!}R Å +mej%܋{#& +Kx{[#Xnx IC'+byT{؀ iC~@yp#@vQ;Qه/.TQr|d4}-*@Ud<t섲`+SҐ 9"ړZUcEavqV)Gp칑 8 6!Ⱦ.(ťA -H7;Q_qe_LL{rxܡu}S4Dg^y܇y(u"p b6Xh8᝕}DDyAǒ _^wpvǷ>Y7PN|;;g/pl\ ==ƒƝsb1dLt*_9'< ey%"w*#ĂIg{^Rx'Y09П$Y*.2N\ >kK#hi(cߤ|llCs~~!w&Ña&9i6T[4SA:7U5Vt5,S1xN@j'W#Mpl6b.Qs$>;7>mCoFhJ2 h78'\zUH9^dVh-i=sx -%㇥e!uv BKv1Ev<:jh3KDc)-e @ruGKGIF+|(c>ܮ M{KF(/7x\kUֱAC6T:vVptK!T 3u&ܐ\mEQǾ)a8+L]Vi oyzU(ñy"timFyMr_GDGl{E>̜OKunɝTRHEο&^KWot$eͯKbÇ0ِ,h, BsNR&o87qyQ5!2_Fq3E.NHQ뽢򒡢OTFˊT5Z^[] W;O[Ͼq:p)l Gw,22U leNFƿvF$TjvY ḠdKH:"*꜒voJrvk|k:߷&zATu"ݨ56Nr>2wq2ͪlkw})OZs#M _`r8w")Ƨ{GJ\d*Vʤ-p8҆lv݇=a'RۙxOn]9KR0`9g3ٴ0v`盀}n0qf2ށxYNFI2v46^VK{`A]c{k}P ;JMeBEZږM'M}!h!VuѝI^졪Ш-״?>5KpRNeXAkN]&^h훼^~-ނ/FMWk'oy5PHj] 0n~Oһ f^Bl?(p9^vZʠ|Te\#\(eBs/R_ĨpaGrHd 2S`z~{mN}h+cB3a&H5 rH84QBtD`S&DѬ97E7Ijs.߯nN43E2%B=Ozb>Dkvyy=Y7u tyPW#b7:mϿoX/ƁQB} ФheG?{:T}(G~=;l{4=Z#^氽GGP >/iC ^mf=']Eސ]j!_}wh}8XDz}z+;ފfm[ v}Ս7R܎2('Pp^}\{YGwpq{PEMO "#D OLx=1u Oz1FF>dX#Doş09bJ"u}dLFu"ѯ4do'Q>$D9W]ٗ]GxHFu(|(>2k*o!ϟ"sS/_F} :Wy'h?FL}R*u?Y"11{:4OuQYCHLNJ| S! 9EYdQr:( JC~'[h[c`]@Õ5[dx[DxP[jjaCCE r{ twk0>P>p_T8eCfԭ| țt̚W,Ï' 8z flrQ#`uAx>kʢT5R&c j#3Fl\KĪd:] ]ݑ/o@΂UveQ̣k7'#ƶCG&! ϫ?i8"`cd >1H n`~O߫^CÝ&j>2 T17اnœQ!7u0 )^D??FC݌0,nQ\)zo 40:o )|^]o!#k=P$v;:?1;w1Lp<#T\$OxzTo-7:1PB7z[m߄&, .sTb]LG'BTLb/M3^w ßJQ.1 xU`odO_v2MtYza2QR%2CgbkC=zn됊^Ns< 67lUPܑc3?"q/çm4\qEQ2qvR "tÿTq) LfcI‰ʑ<=EN9`._}C?iKT5>W/2fLdךAOň}g"gUeN:];jjςno~ā`>ż\![փBʌҷ֎2ur2rZ_Ŵx.N/#^1G(tfp3(wC/*]֬{Hd +c֢HE oz(|A+ pAqA@6場" GU ,"9]@.漣F4m~S,bˎD2l}Y{IpJsJKt꽢:h K=ʠYC{I6'zޠ?ށCӗ,|Tn6aeͩ-;9!)M;jTͦjfxqM)MFnEm558|Nm=<ƑWyJ3mrȾPTPMjxl[@r}=6%w8׽X=,E vdoV=­wCA>=M!}@v+=ԽܥsKoG{{g:ݝ7wLuk ;Izv} B%Z:.xO#|rz֣[F8}6uJ׾a8+֠eJo:y={?t]#eމeK=Ra?|ށn4>@ n"l2n"\Y0fm=j{~9ufەh9Jh)sž9i{*䝂_`ۖ&]j8ҔӍQn\]:Yuxr ⚵Av`rtIcZ%d k݁69uIŹj ,x,lϻr 鼾$tc *F[ʆE 9_ +$pЕc+O>DTpfOqNƗ0AG̃?#+w3n0{vp]"诶l{|3 CR\er)-E 9v>JU4EOƪ bG&6a=nեhTj6 sliMfgQULŠuVRer>+ ?GǂxR?PB }\O/C"%wAd^r"e_Ḧ́`DH$ *Z,98RTl8`M‹Pq!7=Š#CeL>)ٺvx&9;edC\K}poIÚKkzK\ՙ2fؐV_;54u#kx?c-Ń}֭"-lkXCj {\|ʲ.޲X#`j dc5LZ,!LS 9EF/ ʪ7l3pLWcޢsQ{ʃh`!;2eSjsk's'Fs Ԛ& ͕H af ϰli /@{S&ݎxr~$״:-wV#2FQfTL;˸5!8Vqǰ'ڸ:'gV:ȟ ͼͷ_33/;3_ gnmkSak[rk%7`'t3}@bkCi|%zxCC{cM{cC|#2k :bNLZ 2Ȁ"`@cȉG&7%䋟+ -Y4EՁRGX.AZM#2uXee>cMD!~-~"n-uvOCb:ɢ)Bf`~%e~]~w91é"7H-w1ԡjKW섨 ; ج_Vx~CI?8u􏎜!}*llàZ(A4"JY/)i@R *TRQ$EV"+}@kڒ?.ԕ.BSD ޏq`Zde{5^k,_|܎¯8Kwٌ'bН0PRV V>l& hw/l.Ǧ DLPa Z+K@ j^Tȷ-z /(+CC+`` ""`_%ɚ¡aQ)"-)NEUj YP ~,)AA8`VBF 0 4aizHY t<;k'S:T<{'j:ew(o涝R_hQ}tj(Ayi'd6hƯ6f-Ҷh&X;SQS}o|xAo)#R~S)ΤcxAq'Qx=rU(e- e][(@E,m[\縨ʓR]ԯei@缬*.J?($ 7he{8ĬGH[H~۞O.H3(YnPWu=9 bHXZ{bRRZ @CEt2:펧bFI33ףdiDLk_2v& ۈ p'(C}\Dû|\fy@*7Km_ TA备 j )UmnقXh/B'`^6JǖD!<2N#tA ! `j@Fm +*|4zɇ{7Q#1j'+mی0sɻ!֮|iwR*Qߘ5 m K` fͨG#xC8AUx5& ?獇~d{r} %ȒDzɏp*/_(uuC  ,фPYu3+f!00hMm ii5LLc>@Je,uD2w&c^DF0)s)j5*kj(.ͺ]~j2`TJ#_ 1CA}Xڃ}b0(Tr廁v'cꝍ [&S@=ߊ53rZT,^l`@B Ocf$ ֪%֊;fr9Aj1_KÈTl&K4Zh(KXpfغ~6X/M&7"&CW_BxfV#fwglh{ 70UQ޳aG;:nD[+\tYu2]a'sԈ`jD*uƤ@P)m78x q{3dmCw^YLsGiqo)?74(#,cN b(){Lvp,GeW= k%\1On{i1|p!FF9r$:GB(V(zo:s$hNy,eĸ.Ud7TH@UV|!E8"1EN EEU:㪌f/^IR%uҕlr%U)uŤwE߹?( f'pFĉt>e> H)v&n+b؝P>/7grzH?S8# Wa*6I]rö!j[Lfk_(T<%k\఼7Tx#sZȝ'HG:^:QSA6˰8Tf%FNp  ,aP"̈TEOgZեh4Ƙv_Jώyq?H%['CUGV'2Bv<ͩ ;a"D rvz$#7oRQl(xU~ RʮaM-݅@/(QLFb8f#WJW!VqUǾFnp~QHgY[(2Y`<ӧຫ(@O'f݅ GrjzpdOKLLC u!g#є4Pic2)z_ѱWFp>zB7tĸ4p9!M˷No) |9 XfcD$A%2g'\ [-l*RoV06$M:wm{#8O5%~_ U5BCL1XݞJ]fbiOM5vK+3K]A 6h۰H}2f~TK5UNLRdl_VF^Q%Qlݳ7Z?@Mn0m ]K Fb(ƅ hΊ͘CxI#S#^@ȯq,43> 7ƪ0IţV_32jvAN4;\dzdʲN"7ZV#‘A eK5oq]Dzz;mŽuZ:cM>W2% J4* SvB#| gq\ tpäF@= Nl XTil|óP߽ k<&>@M }/$IR!`gI `Fَ`I2#]H0@T*y:-{74*1;b? ;- N>nc:E51)4U4LCaNrPK }pRˈ)1͢I-C[4ETE.L,i;M!pOvY-o7lhgv!kUaBug/<♍P=̤) x{ &w"}xwftﹶ*} A2 ledF5k kimu{**ˍȨkqrImGI³:yI@'0cvcnI),w\ܲ݋QϬ.=uI#4yIҫ'cv:Nԩ 1_<:OHg]9)Sh#|z4?+g X5#G [eO}U_*:C[ej@L6KZk0hVoX֢8]]Ae}|@%Lk {fѷ0yg,d]R⚍& /--p⦧0ޖYw:Z*uH}̧A+]owuϲ1G\|a! S(wvDx] ˵^1>n lzmQ]b|4E3#J Ip ? \o,U4R]u Zkvy;hf\_.٪x([pS ~ˀ5fl''z3ZDa?oh\ :F#d M'Xwe1We}s5 NQk5q %)PV]#!Aۿ7RώHV=2t y8B]Ú+v~hĂ)~[Xw/8Lq,~EuSlmۇyu7 2%;F& pFyMw&ΈF ,U'$g3$BfFSg&{">B*uÆ; s,ہK܄$ 6YPP[m;mH>EIZzoO7$ v&Ԡ^$kpՂ#s* Ӟ'u% Ù^$9-: Gj֥)k3b;%ftnvezuk͈P[p(FA]t}25m"gr*>h~PP%룓3;b .ӻd/=&6QdXg n]zd) oWq&[c6n^d3=q5 +j"4lP+m%2sP挓$6LZɇȤ`bsVaeKIg \`N#dbಐ!u|NaL3;@6Q<%H4y Sol- (\sޖp}0 `vc:nTf7;vh|{{g]sBّB<`x'*jo_`SM,$?.l,3Ϙ=ţr)@_/ UQ N3pBڄRW ,isi1/L'ho/^4lac$P5O,Y@ Ɋ㧩lPdzf\G:lt`d-&_L%)twV mS7We7o_ scQѕ&[ówou, ODz@M8ؑ0OV?b?f{!ԃ8ŢNԌ(*+@W8ho3챋.XW*<>Co7e%IdqG6XgTJ4]LNL}ɑpۨ|f~\n$(_\Ś7B$MtJz6殥ړBTGo*fJ,MS"*,YOR!cU:)ld_%M'G oĵĪ5L&z"ʻb5R3eR #QKi[%tK#i U۳SofHRe)N%(O6<[R6*tz'G6ox/@",Szd, dyJ8,G|ƽ G0z6Zܔ)!  bC2>[@[NFn$ [7>{ae4gy쪌ςQNj@ Տtз6S62Zk5vukݨG'M]b(W ժ^ koڈ~T,X%).{WO.@'~?C=~* uޟ104[m8ˇךaw.Q5& pw5#̎h{ݴ&SASJ}~8ДqϰE;ԣ3"^nxt`Q7,ҍiexG:@^y+vt JM0K@Ё%Z.Hb>3ҫ;&V<;Xqr.}+$p ^^ݬ?V83PO; II`z׸|:f+7,.,e @Mh]'`btV#cPsˡp QGRv5) ~0U7Ӈ.9sG! qN'g3ڑӫv<7f->)ǿ878O4B?Q87S,AII^!N`>yv!,gwN^V >Ő+p ,7BzV_F/m~t=JHv5F;Jf/xWhSS >ż}Y.Z#NiɳswYkbz]{iJusKf78ƿY{ʜ~ខGgLe,'wۿ4K&kkфLخ`cn<c, %~۶m۶m۶m۶m۶m6t3qVfXu2T&aӫ |➽4 d7+/,6tb_..~:}"11)JAՑ?4/rPĝ37W:uf"G (,|,it-eĝRAk*:kwQ/<έr,e֚+_ R<З{\K1$e'z|l*cslC.ѹ[ac{qԨUKGWoD)5Rb&e$0(E@c;y8ф7!bpߒk9;=c)'1Bop \j|3޶!!EƤEV#fv|}ICWH֐w(Gm?\,6yʴQ(`E6''h&;L11}}7ܢSȷrВ*#=q,lqTOHCh܎V|K%GMm&c+lf'h-G=Q+&%[a7nZR:@+H_F9?A_,iYBv6v."Ύz[Eu%{L2[E?ӊL;TSTÏOQO-KJOKNKTKKϚ/,8GVLH),nS723695Qt 4 d\' UsjrJRirG^0acddoomc=8l :{6)-~ȿst* f;^WZ yA+//qN 4b0ŏ6'`%`gPGoԪYH+F(mWjY\Ztڴ|BHcp8inz}Xhz[?BHsS ]UQk](0 \.%)DiOw P̨UgM( #xC;jє<.s &rL0 }k(ja_fG3IMx}9إAbGɄXS3+ iAӭ;jdCvYά O>l 7$>"h/Ļ3-R엮] 9),r|/\*y4H;4]l(ޯ!}aS2 u"&6\h4]%1MWdEʓH4T<_`CO R\}\Gl`ʟ#;vLĚ dEwgNZ֧&3F9gjoOtpߙs R9-$|>yqYt#hdARKޠfm*2[9݀Rzq;bC% ^C$kЦ\PyPU<#.:پJ+o/XжsG_D)4pSф9=7vI,M{e %`.(Ibl R:TL1 :lZ_=3_=.]`:z<:H9y{ j_R$ 1ugc;(Zu%lx[`Q L(7a?ME(L/[t NM:)\FQVɒ=L`}rvUfJ0!o(!Rt*`M@8@FHOR#ncȮĕb "g( k郐3(N/&DR#W\> *܅ ?b5eT%ô4΃#DzcZdψj[1ܹ<lMTE[I&,} '0&]qS ™A()*>a#l'{X9LwCԮ,F (PAny?oh|NաLLtb:i=MiHIW6#QV1 WcvGus =K|m.Yey{MœH#^Т‹''&ټ RWwRđA^x ~6퓰W ֚> ,YR*ɭǏfbj9B5zm8ܓx#=0X~3nq;M\|_Ta׿2)5,P6l /HD;`s)kYj'K`D>C dR)dC5TB|!p)+,`:?&););'`0R_ 3E] ggg+-zDpe>5׈ebʗ]O kIl)p_>0xU<?JBGJ>o.wm=7TY;4 p 7vbiЈ(ȬoI3o.kӯ /0'KR˘W7o ^#2ٱϏ,2,兴x,'cJ,qcY@y!}=+l$vDl=ռi8> rBt&7N͏UNaìm%Ez!F_]D2u$fX1P*"zg]ϗ݉;^ I䔿~( Xl,g|ITkȁ~m ?4wM F(1e0R+ѵf[a6ȈR73kw>F6^cGrSWچSMw !9qLيje>eK66fmEmޮHLFȿ/טRo"h *24PLo)9S"LGvzKx ސqq;XR1.n!S#H釞ޅLϚ-'5oIYdNP:F6s"q|NeCؓ4M4N׊RYl]4Y *n?34[.QXC*Dw&ˎOt/R,Io^(UW0>@guݾ\Кev[M?B޳@iM~? 0[b[{ yiL=\Gn HT$7L[h, }>*HU/02skvja^ZHm~sC7B2Ix{?ψ74,9ǷP z`*`&~b=Pձ03:`WJCvw$o%ފ)̇·rqS̹#MXsu3_sVv~u R=a+tķb@}$Q_2\oxoX׹XXy2bd`gbd*͔j w+OGgX;e #U!N'cf"2mI'c*E ,e7!蘃oUIe QP|PR"]ȕ`_*,(g*7!ЮE/-6 nK| ;e#L 1}P` c5W0]iC9  ?t PBZHHMPc0%44&L"aְۡ ͞iT=Y۞<-o -M6l,[ҽl@rtLRlм+.@vEF>Bxf)NVEBi+rsR>Wcҕ25C4%:&Z;&jRb8K0b"~c.ڂּ"Y?ۃ\5w&3=tȎ?ؾU8$ߔ⬥ _^(u݀l+-m;{^{$;TQc}O $yaPb`UY1B{_;\ _͍8 c (L8yJf&娞ބ(k5կ15[;nP`mN/6s}G[c>BFUeґBO5++Q]C :2(d(r:c8*av!Wlu?XH^,f|.U^2#v{BBJ'3M_0}a)!7<~y&QKT+ş!jN:usgK+fp]"ԯ;1@6( @dNCf#|13bT|ӿ-Qݶݵ+1 ~$ w{7iP+'y%EcFmxRQ҆}~|ҮJ>&,2q'9m Igު֊ xVe-$Q|ɦKjZ9=_T)Fw5^`"5ҋ>& 'R9JIr-KriE1_缋>ʗi/7=7k'(#;+ePjC}% ` ˣS`1>w]7Ƕpf8չåsÖJ-͚6)LI53`GpBW~W{ O 'S|:Ytג|c]SZDW7>|Ky%aaT;w$XUm"ښn=tnB1CLGV{i'>q6J.~ej9;i4$;vu1z#0 [7mA֣lj|赔3 $6\"6i {ێܤ5}go 񰴢õ]Ȟ~W`X-5GD]'SW {S?]] 1w%6T+ffqDO1Cc )CJR;N$nҤbqDtԷ=C) (>˗s[o}:n=Ei|l@.z.@ku4O@vҞD~C"FU{]ּ)6*mnV Tϻ :킣#~bdq|xq^d)_zf)|q"n/KQLOn-+|i#nwLգ=W7Iqm7O®Zim_ )+D0M+TIky6O)^N\Y7ORmH; M?9aK6bXa08T i!(QK@M) #8"7ž§G"IbrKMaF' &,R_˯K44QlSQ`Wu&ﰢ6ˆ ZȶLĽiqYrQ|mt& E;Ҭ$>K[P;i~T&mYLX<14Q.dYd̆;v*DǥhۊZj>\ɷ1Z썶;6+jPXo#[$yCmetWib_c%> O~sH(Ƒ6ArkN0DgVkD[x~!kCq saKT){A'Kܲb JQ$#:)>+i iDJ` r &u)iX1d%B: :؋uyIH UzOԾ7@;ŬaY,< , ՍU(=;;f 7a7u= ~%S5QEv{u>gT沔q?̸>ʅd|&@NS=rOT[.t-;y7_uM#-;mM鍧Mݿ4qLUQ.YKyPfƃ^Zcc r+<gj>>S6wNZUHWgT+Y+lb.ZsQZvنQ/p *A Hwϱ۲?dbg`! ovvŰ'R=98 QZq#`>h׆PJ±z ThE•cK{MS! AH8VK mRl|?Jg'+3$<d>d(?x2+!,Qh v7WZy)"nX8vj\v̫VRV%xQTqU8X"AXbOpR `OtVsH7%cT?#-8.p Et`^d5z%Dv59=J6lJ%7_s>&u(=em* w^|KlFOqNVK%Jې6b^%!a.l/lZf$aylj,umu^?t% $TըW%ի0G<IL#yHTULx\$&L{49us hם9]j{NJ/z@5%_]5!:zߒknS`z| !%G)żE6"&u> 9jW9nj_TuЃ)>G*YĩëIb.C'("ϺJz_}S zM7 C-ἅePɖq.a4e |G XPBq9d ʖHYd6VubWyvs`UvmMT7W<"2c3Ev/YAZ=V._P_t)=ha8AȡfsO߄_]+r刡EhߊwĨq'om99qˀ_B?Y@7:$пV L&Jm_"YŬu=|?Y]|)&~#JZъ3:, fi>eBكv +fan9"qN5Kk;@nL[_hP{ȐE!X˜K0'qgL4Oa`¬2G}nԠhpQKC УT$3}e=C"$om$J dC T32[YCz Kɋ'JQt "M8bjU"M16T1ceUhѢ̥ ~vhc<j-b<rvŹ%Ϯ`9Mҽn🰃`XV]3b OkC^>dO'KX48u)TQ?a9]po}7&$#VX l6XGu P[?b鱮ӯ,}ȒpQhIo.v< ;Gcjt OoY9\0M0=D\I8|o#kw{Ẏ>]o-g%ьJ5+:^ί9~! yW3aT][⾬uRIt8p 'IS+9rghp`$IRhUˁO;/d7Cw~"E59şTN꘤6J\ VePaR* Fo!RXɁ48[H"֖b{nrCԦ땚C+n9mtjWmhej\EK9@3|vF]0HВ:3OMYj|tXl|&OxSoECKttpwto7/!T?: 8mK㿌(]" JJM6#K^DNKaJ*^R40F1;1f4;,μp~^Zɏ2x{ޜl4K\!hyΥ6^wzpnSd_Fq5]3kq M.STAOL=u5jE_,ܗ.*%0ȁ(rHyv?R 3YE}TYHB+CKA :2N'aL:)a6tA(8$4L ZC檄gJLsYY;m^8Fѿm\%峀C?@d-ȯjIQ`?YbF Y?K 1AȕfR]&ʺes쀙/x(35~a2:Ĉh;TKSQ t90x햣oOa+ jѦ巑Th7âLK [cBRNK<C^\AI}B{~xA~Co}Aq[㥩ax,&[NV3 F!jLiĶqup݊sM1\~;uO*WX RAW9TJh|W@'фAdt~I60sqKIc^G∆hY$%\\\~whj5TSq:Ԙ綠džyh=!jX}1{-f(/zvh?巽93@>2`)-LEIOXZ+& żZ)F ߃p553#} ҥJ .ݼI]i*e~CA/s̰d ܓh\ށH.~8,#v2fx1j~J>U*ic>vY$!17m3(@΄i 8Ya6Lΐr@ʢْ4NJSK]?&k9**E!a$3VFP ].;*c yG`+"k vĿpQrG\L7` W8z;om w`oEvhC<.|‰W#EA#T>~W  0cic?Q/A9tɹɒI#D*<: L9~,1eC.&UWNFY(Z4ZUpP91*GYyDRr{OPʃpA.&q@){8su W#>hi:f:%  tQ՚K5e+++_OjeO?17pT27W!G;iwy:To^ _P_<DC0F$RToS\0p>b< .#Næc|kiCjD>vz83viut#H(0e x<áv*>ՖԲŀhAeq!C(HGZg\=4$= #  t@#ݕd>\a SX +"8V\ԱaX3сvkOBԊ +QsD2L!~a??K ) Îsir}7(\B%dˮ+ߘ̼OcYPsh\9q^E?фa(96<*b;!nxFX9upD4IzqpH7vgbnzbC dZo!W|$'WQ4v4g3n:!ulo*d=+Y\2 !*#Iy lBi6GGvFVUCKJbd)gR1г֎D7ʁv<7jQ pKz:^ҩfZGPAaPt&oU1+,ιIf}g4'C*$ #H*qظ*錶H ZSzdDx.~oRfMٳjW"'RPr^ۤ]}W?t0"o+' j8z#'8sM)l7w) s|U[ET.niz Jbq.0z1mkuvs,,e`N?)& (~hֻ${h;۹'sL8 Bڍ`w'hjt ~7᫗K'k@i{qi|:glKԐ9[h[x,clISuCfjN˚Q|? ,eϝiJnS^i,:5č ld}kx83!7S, ɀ4=2AЙcRjtdAędzW,dAըu5# LTd3nY;(rx >,LK%ШK?̌)Ѡbȅ)~;g="mFU5Ij(Z/v.1n[Ϭ4&-m)<\|s;(S:Us5f)>c3*1Dvr[b-PXlv.A0Rfě}Z&~<ϩ;Lt3cwqLzX[M#;'w%+p=,tH3]ʒK`yTR8EɄɆ/!Aohܒ8bl$(˷v)H0))TpG}Հ9*R TpDz42)92d0( Vt 9%m;l_/EKΚzӓg٪Ήu5eŎ(g^s*2 93T/e`zӎRcό}L.g0%orLh˱I,SF±fFYMu)DsRX#Ht*B"أyfZL) vԖܺ,4|. >nUǃExq FG| (q2%e.VqxBZ_+h<|=Ckv(P+ᰔ%m'Gx7jO\ް~a3T|g 5݂{F F/U+Q%Z5p)wlN7:h.8%4wM#mX!Vn0tίč4 9-Ű; q?E5ۇUvI߿hPx;^P$*]pk0A._7Tw> > */Wm%1WF$ampr+=m>Empr}+cLm~wr;ɵ٢$}ou"HU@}V$u[֣XuobCxâA"$oÆC֊[I1ଥkFPI}I Q8|)g"{n`Bs.%nےΟ =~ziCʇrJa}M| BVB u}` ncD1<$)"$eP0pafVa  LY,@ 4fh p"ݙt{30 s[a[͓ s:=Ҿo#yFɒ+vM}pqЏ5v]&g@.c ʛoЛK +vŽԙň6'?i>KHĂS1np^ᙺ9E(H^ت<Bf*%ioZ'2Dx5n#QXdpI9k()),d]QԆNԅIM!;Y6[j(2.bpcB A6'ptOX9L6@5F%+TV5ƙѺԫ0<6e- 'Gw+(! r4ceaDV`LP y1ɛ qmemrmGp`IUL~RQWTb|E7(U[ g9uyD.Y5hτh"Ռ#7,O.hGEfI7yhگIlMl徘ql^o9"aY6k!==9U'!1?O:8Z땥0:4-:8e^F(.:Q;&% ? O?L}.2>w o Jl61%/;w!%h_7L*w@YK6~t}\toL;sK|h{*2>+/V0@ҪmUJ^Pf!ޮ%}ro-Hڪ%yW3rAQ׍@DHŰL29QSX0pcZdT*wFKDP׌Ui'l'bchH((!aޞME%_&G(Cy,)׷N[?IN25 sz z))H>7nQ %`wv9+(*#`}5r*Q`x:^j]ln8nA\6X7caA8?sy"93;SUs\רh .M1b(eFXS{I,r\&;2vsxfN%[}.-1ɫz1鋫v%顟z[&JpӨiFQp-+%պ^!huN:xxM`ޚmqҤy6o۲;2qϼl;GsŤ˨R{8aW{RSe S; #L;P4z|gs%o*A,8?tS4T({H]-;P1ݏl}HU47߰5`ok5HweԟۮꂀH-gH$Ȍ (T.*Mu i2Zˆ +VZh"@߷>ȩԨ&3~7-^7dtI"pa}wTؕ{ u嬋QN14)ep M⇌{R8G} S4{pP;sz} AVAz]eKIuթb[YK[ (j"2 i%j"k>1l`"@L DIzQ VÕGWPF 6ad"A(0rW=w祃0I \[1urTCIe>32+٘7xCU 7( u~H~!ypAdY5m1~'r nJ3*IRH8X1ߜ&K2/ =2h-g,"*~3@n-%HC2=J1aX[B(OBL>7+5zh ($D>ۯ6  hHA,`YA<w<1B4ꯈBTlÉ`3nbbc hڢ3Q!W9oP~y+'j#~C. q/x:wgo"E=0`3=bxj[:ֹۥMO@DeqIߖzvφS5}'W rڇeBsw>Yb~QV=uUp":v[V=t~6(ePIQ )ۤWI-(6 }] o4% z{>#=_y[ս~ѵz {8hg"7+* yvUpUhӕMs=J~B?!D]WEwvtBCZ#}FB_'5Сŭ'KCङ7{^L.^O{#ԃ77u≯B$%lex!-$XnNiLul~NۤPMyü6 ~~L9n 4u>We>WF#*B2 #,4"bm&kM,85nBO ǿUVfWglþJ*z!+lZM!@a+:j.Μ.{~{Rk3e̻7arUw ,=s޹][omג9Ɩa1죣xZP. q4;}*oDy5EF*n҅x[:BR:Xqt]Ynf#?::{pU hrd]xG|hR|~[14s[uSJ&.}s} 򍴢bų%Pfl|׹9+)7E֓:عƏ(6u0Z$0kV&Ŋ`\ﳙ8G16ZXz_(G%rM50[ֻ%db)II51C=ͮ21xQ,~V;FO'WJI[d1q߆4-i2^T1rycڌbFR klGQlgx*Mb&';AӃ"fEYò9SFYzt ?d@'pÒ)?F `GkFp桅AE"&:#=+pOwSA~=&KҘ]Tڌ UVMIHϓqИA FQ#S/r̗hj4$9޼R2ѐQ>NTIHo;̉{0zP Y|PbK\qO^mk`4Kh`N=flHN=aFaw;nm.^kL%r\my5c1R=nUJ57nޕ\-i|oECOldJ^P:|jt8'ήʇaObxn:e|I0=c<_z *.jXOQkym. \O)Bco).9=l@*ERv LdBU2bSifH\7h7SLM54T2Nώ0`I'X,I+N&E-p9l|GN86(Gem[&5] }Uƶ9XL [)Qj@ 3ʆS&<·vδ #&P*|G\I. ◧ZbA//O|h6ØVlTu&+dɂճ!?o. Kcb/),VEŮ:50 6j$P!y $ Z;Aعڝ U?P>T@eiَZ#@BcHBr=Q'+wLtΝ>Q2+C&e.BQ&Jb‚ V@MMZCMT+tb Ϫ> ՞Rl6}S ,#{f7)=`3j&djdiUXRBM3Hg)<g[JA05L!"h'UZ߮\> "F,x5ahJxXW7uސI%_ &-R.1Yv8f[S|"H|ama0I t`x8X>^ }ĺ331;I3uWNi v W7&߳CX(J{ hG G#Q,I RHfl~&T?]u98{/%%$NL,Pc< h_U >}~sYe%Niks}ԧn33&O C|l*lρj@Qx3NB*B!{|+$C1D`b f 2`~ 9#f"܉gQꡋ1eE`4nVYb,`Z_C wĨnaa*B6a˲$tW.L!HvŔ 6+F]nN*?)~]ľ:%05$JilS;ROO1u!_ Dĵ:ŅANg4w8Ba\uX\L{o߹Hn1?<L* mVBVRBJ4Z~W2qW9 5KҨV'|-,f&/ț_vn}z6i_ˋ7G6/POP~:VXbBp^8 ܃io!mKcfc=9N#mIAYی!m_Iz]JVyEiO0,Nt|J(~)KJ[n֢z&jZA:%޳ve{M[7^[*SB0ă;  -ƁB6  $0BDQ޲Q[8$ l؅Rc8 )`]@Aw=}tA(V#}B;X8,bqhHIYoiW yo%ƭ׮}{װe<'0A1 H`,G btp>s1[@ꛧ;ѯr$Ϥ 1sv%=6YEw|G Egw3:VTr/u ;e$ӎjp#_q/ ,e(w[1ѿKekKxwu5WXmvv(׶EJa3ewUR<~ܥiT懃#0BL;K>g`A"}U2y #I=UԶ|{;<mi+Gƾ|bwN)%0zKWCƜ1EJ԰r>?piv.7jx+$gch{AP2C02 ^r=1l zl.s%N\ ,Xs PzQQoLEM!\H~KKao}Z`:hs "x^ֻzStuS}rb+/)wۜ">I=*;0%^+0奋%fJ!<(ގ_.N3}~,Yf'DTPB Y%Xv61̢ힼ+/ ?00 2$xIH( @8s ~Z6Ұ6R+@үy.0-rzeӦySOh5/_|sV{3+/: 4$tVwI9{ bgDe}xÁ|My~Q*LW,}|vmsrU4|Msxtu^;:U\( %U5O?qCNF+.zydTpHaUJgC~=D'58 O?!G?rmy|m<\3-9psL->ymQ4pX),B&';HQŽ> cO.&B{ИcA}&'3H60TV[ d ~J=_T*pCDRaFp*}&;89 Xr1ڏ>eieDH2 ƸĔs %gƣ S a#^=Ə q Xz䰓 2\(N 6_0ec go`4j-#4e.Ecb~vUк@,+sk6im7M$4xZCdr ?9Tc]7{I7QR?efjAh'5f=gfGjɭxz֦0AWaA,jmB y›ekHIk+~UzG!'X{HV+wHَ[kX +,@IYsBPӊmlqROk e{dn.׏kW;(zR'0)q?A+h_C/5Κc=jݹPx!]LʊcF+<9J{{ 2fw-w&tzm+yIk!"T-dT Q98c`gJT-ޑ4_uW ^"XRoz7?4H+FqL,[j9qaƬ9(VE1p7!;:I`k4{R,9sH1‘U8KG[$t`QPۺ] Xl-kWhR4QҺБBȾF;FE66ޠF= ^oi@vq䋭d3ASS$r01D}/ܭTvQi&[Z3V4,mY*m3;%r/z/%e-YXXg#^xU4N.y~N]3BYl]#{CDȀX*OsaYda.c}9NvE%~Gpt;զ%YBMٙVo\ @cU klg38WF2rI.nrUӽ$A2}Q u+V@@F4>"qQwK+p/sR*uJ')>vm=×SB-؎w1{a2ɺ:[Ssym%)JՉ\n4rK"%Em{fTG]c8щ1LLVÓ8B I tDkp^[ /ߛR#@Ȗx|2r^O`(C!xk}Q4aBm 8`}6nn=.XNTZ7署50_ @V5ZClXzU=SLiDVqmWlAEk ŭЇ-0 [-Y|[D V&V'Э#g[GpIvzׄװwqۆ54 IY*5+7+@zyyZZ5bÔUn(p *y~_%ˍZABJ. ֭C3Qn82'?}QUUmᵆ<(\` 5+էlI9!.*yzk"p7LcP:|D%i$:M5r{Z80lK#n \NiqQλ( sCJS#; jR뉫?9sպұW8 WY{@J srr^*PRWS>rc^@,hbl8QvoRk:*٢.N9?`Dd#" 儬d Qޕ;0FB٠E C SQ+ ʅ I\"/I 9A*C'%P<JЏߑit$_F&ZxːfW{(GA1($&~BtY}`LqlJɮZrtYkO^ L*K;9[a3Bt#<,tQ@lxDb.oG YwީY)sM,H~c?57ծ mbiaJr?}к@~q; _LJd1S:Q b9@:oEu!҄*bz*gd!2]!D ސ],LmzWt&O6.Duڑ~]62iш0RA=%ҊU鴱mTsEh6΢#ԇoB4aS QpfkgNE9c>6,_ 厺[oB'u#[X _[z,qtk89 3J2qa-лm/9 *,x|~0fM*W|"M(* <ĜE5R̨y! 56u*&}ON>sD88I^kqbI$SҬX|%z!&DPBK}{OꫦLsVD14$W# UC@-3r n6I{Q勴Ց^na)sy'ϏE YYm[͵vgEcǸR$:JTn􋡴C2{SPr+zPc~11"g8ADJAxb/l 'Y|˜R$5ɊNtZ=zT0#A nn֟p?S7;cԷuHO\`k#n(h7Y^Т9EOnʽGKctLvlVd):?uܚ?#vUNxý̛`ǔ FKwz 0 ~Y1NE]Dzβg/(d㴱+5՚\8[C!1jмsj z/O]1$I};:yjecA&o(32@6 SO +;4gnH3Ah%]a":2EL#GH3L&ѿ!3F9bozN1X8$LYoyOgs^pѩV^ :OٌZojJZ5w8B 8J+xq%k:8ܫn*%9M{iMT]Co i̢J$RK2dk3?7 _yn;)UrDa$;|Gx"$\eE킱j[:6)`}90V͘rf ꫵ+[,8@-@4鳡F 0O9!Cu- ܨdd9Q.LTTFG=onXŅz3VBj 5k cem-hvqZu3_24}<<fV\$dL> Dc<*R~awNH;%+,p4g)qǖlC*n7}Zi!zx6X~pJ* mAF?Ss )" dD@>.Q͕D)mc JcZ,q9>%0}?0ѵS}*UJJ12۟t >ď3ޱhI=s6hQj)y`BRƊn |W98W c-ᥓRHk=j(`?ɲ#NzLXPԙ+oX,jMax4whjjcoڭ!Hʛ ^;^x+(5Tg0s1|Iu?`ϓ.E4Os*3Q\/u)I'Yن›~jǒeu+n p?3?a&ߒ|m&qu:zP[T3h~#P'rvK[sY̢SZ`f %4tK#>̅9Qc A_^Uэi8f;<_WL& p6Q}gyp(dA@_0NDX*Rroa.;Nfq{h9h3ˌ.j0TLjgA38C| :; mÎqrm`uw-ZQ~ß=_/P@_b96d~Zqʤ ="xc%RMK 5b/ɞHuU2n[M= _#]HF) VL3h}]d,F}ZL4 G >Ct7@ڤ{K ('yUJ7G Du6WN",bޛTmGJ^Oٍ>/l`31"9 墳b @ 8W,9SP[{6')LlGOugC ?bP(sVf`<-=!Ok޿kǘ5$9 Hp917~ݥe፧j$kjw֍KfÇb1O3Ú=X~>T_PpUT/_rw}+^}>BasE]mTܨuSjĻk׎FQ mqn%#De{aQ7DY3zҒS60ޅbB E~Tg^0[` AV`8Lm4Um ة&B|M2=D$@Eھ(ܠ[Q3KOˌSGߘ&;O<Ąz Iilg2 JsxwEf{l{]>*F;oƈT^Xfb㱯ƴH%E {'Joy,S43Agw07B|q}HYlJŘs`^PO-ʰ2><=U2imf-,I t'1lPb;D䢃A4&aC]Q@JL$I.!YgeyL q;Jnw[ xj:~ڃ?\;p_mͤ{0m^kQW B0ds;s8rhEadLf][ڿbhQӯfQ1@weoQ:+ױ1KECa'l ,['LD P%OMs{V+|HBA#oK"~)-`Staۋiد\2y҇f)l?6lI`Bmg Nb3j&L8mT $ck3¼wꁐ÷!tslΨHl:W7tWyWf YVfUHWX<70QgʇUQ_(\ _`I"S\za`:*[/e.dxw ܢ8o28.x2-!*FCa4P`"2=߹qˆ{̱.TTɤ$QJS.9iZV<2/H-g,@8YY plx+„8g%Ew̦3ݘXD3z^TVX/2%L} u8'RyFDI]:-2]B:; gi+rіhl>fM{LѨ&~PHFÞc A>z.GWfMHDE {$v Eh>r JڶG8ZFSt Ѽ CR??X]rU¤ N<< F e0MmO_NOϛyh4BܴԨ,ċA0\a5ͅ(Դ!bkHIQUOƛov+ҿiO!%yT=d{89^" 58[4.p97-4yaHPw(W !`I"5-# |zkIU&tdf\`.:%J0mvG<|_K]}Iom;pw ue挄'}{.z{4P%w֣[[lHvJ3P¶j\bywf]+sD5Xш' zjI7s6:cw5)h$SXf5KRwZ\{RpkB+$^KCPBÅY:CwϬ=Z&yr$\aIqr2Q/< ګ Cd8m.DKĖ`5 df92X5R5d)˻yV> ߼@)c\e8 IJs㜖nˌHLp gD޾Çz`̗b jMfMXI"=[L (0"p2XC͊G3ӹsMKhU#M,2Esѐ{Wshu^G0yιAO[-M.4pXy]žK{[_[> >g[>zJ/ ~EQ_{,S( sU6kWt "1:ܽ w^e:POі9|Tl11E|84ma~.兪9y `^)xCB_sCx3w9s&J`~Fu{QֳM!(} d'ܩ=e$d BJK&PcQa.x *,/Xҍ-GT|bJ{;~7ߵ´f?Ix_!EݯMζqsx[ 愘ZLvZhR%-\[i\" p/! @ӹhӛaFD4"M' k8)^HL~t,Fw \-Kac ) )"bWp-۶TEC@i=!þ!|+y<':7Yo 'ǒ_ +[VK`jC,$Y_N>6WfGUz/Y*|DR̯G:U+G:2bSy2 [j"G' mC^qac7Cn[Pzaaݰ~귾͊CTXjR,h*9fsE3qF\YR?q:]]GAI!P AlwUćrd4HvlH:6:͈p}c$֫b~䂮iA슜!q9 zJ{qyAPau~KlArGmI n3]#]}GfOH}G( K7H+(-5_F=gW#\OJj^R|Є/T,YbЬw8lU؈;w\O4VpkTUĥIX)vrc`*̽y~E:)Kz {RJN='eGJJ! P(QODTUíe+OF}Gm=9&⭃RZ̃Kl,XM2ZaZq2jϋΛ?_@^A^AYeL|!Ȥ CErpWBX̻m Q1Ѕ̅K*Sdff&31Et0VXOd\`otZv+e` i_c[@2𬕙U)@72“ԁ !ʘi5ݯ{aurU3|E,㸣{tr@-zRf+L&#VQtrxkQi| OQh0d}T i4}*I>FҴM]UE}Ey:YEID٨iڝL&)]8[wiG'Kb,2HeVFw *Wޛ}F9ٴ!jIU(1Bqm(KhL@1o`~*Hϲnt߄rRm0 xM E:-v~cS>PeHG_iyj8 @Η}aF--SO^" 쀿lc S6F6i yb4;r0@xhKL+`9Z+ŔG6ؠv >}xT G1\}D[j{ڠ珬1ѠͷEW-ƞ=#e1r3E řA[.`p}^y ~HlO}y遬_Ē[tq̀-J ێ%#P)~N$ 1 &\ .[lU91^KʍwQ55]ܩq_DQ\[_B<JᰨևepY yWF:.6d\6Eibj>H%Px"20"W!ˡCvṜ_M*")t@jPqx*ҠFyD=:~p,2~ o7ǡ|.s$v 8XE0.\-ubX*wZ6;MA>TcFAevz3lBV?XMtO4r ?exK58ώn1Y$AFW avěȃe}1=Fy=TvEIwнN;7uB`Ѽ[9[s|ֲ $Ӷu%[i(n rƒm[1c%/oB]piBGVCF7*|:AkO3g$8JS<wpB #B1''L})9]"pm]cmqG/[EcKh3[ww|~Q. }8B݅UPX갆ӥ栾Ssh7WZ]$X3Z)Óqf&S>`at{yzʧ}Zk^JS"?-vԕY*S+a]elR +V? }׍5l:ds-o|b97s=tDּNPs?OXԾ-L /u3=}7aUI*w9ih Ϲ~齊S+H6P EMFUU.X"%+L^9V ȱ9m!wg 6I6_ZΠ5`q*rtYVzР2/+-óMbe1xurW M |̛*FqGր֞Ui 7]4@xI_[K3&O`B]؅ [o "MV#l TWK~.w>-}hWMm07>Gꢸ߸_<C,I%s6s/H'JV&RՕ,[Pˇh4X@M ﺮ-7VpJ| F%+SXtuz"Sb#u! 'G0ՠ1z5+5b$u$pxtN~55 b s0B9,Q|AhH٣sߑ;LupEks;!hz"ǝT@|U|ŲK'CmK~]cE%1)eӊ9I*{_N2,nOVpwjDٲBޣC٪3@0qV))\yR~oJcxըBJ+$؄(@˱icCoRM>*Ar`Ww? x; b6~АĂ#B%,x|+-̑p#Tk |+ kO]NkOqĪINt0 nȊO<1YߍHޡ]f oe#=|[\hŦǾUpr :2gTtPCZbnH*eItpCF4 @f S(;//jm2:L8@TY3n6]P45B($nUqJAb),m0,}!MzK yY=O\3 n"nqvn=C\mv6[,B3^w%D-Oe_GRu 10 nCsJ  Yɍkc )&l6u`q 3Л0ϺJ?M"w]y1`ɷ$a̴Js^<^X>,3g%v0W2(-IT^-.94(7LB4"-EX @{l@1E#k&\Q)Ӌ&\(<\ և9~<162BaԣfC@L{LKF_ts35H8 ,<^˨@V*UχQV^u0af4_ .o6(nvrև}IPm>ҀT@/mR(w)H/>~#̈́ ΃n <0gX@vUڸn]Be쎒OvQdIar<+ Mf{+rrW[…V 6X"p7Iu ҍ7Svb6i>;2x_aMDG  5il>R-FFz>ڔ,EY*ZUL\j% GTN'wa.&::j|%Iw5nUw5jm?'ZG [/]yZ7/6Dέ7$ۙ5۝K⛷Vxn\FIk;qsꡝ>{I$r} _1&a6ɤz xNᡪ-oS]Nijk~t{fѬ*Ҿo+VE3^ΰL`Hq 6&v)xV Qi" =;;4 Y1R[]ϙ\qʔ"zRT&i;v=2Û frle+#e9+Qò)QZgpq1"LQף{X>Z/z)YW6S +> 7 >T<԰H9~=<*, |jF6[okQ5u:Q>\3T F=g}Fxkoβ y1kH@`;")PKOAT)R)w̍NyF+ƅEʶ/4*etvQ bs-€qU<,VBiveo)Ɣ)9 Zh_CS:* r.vyRأIMC[@-{c>3d,DuHIQˁMjr饽+C{ۃK{HA\05{Bn\?=ߪNJ{ַ;F%9߶U쯎??tܔ=|^u{MP}]/x+a`)MGm4 fnf~lU)tC R,3;UD9AH˨sR fv@6;3+F\HWs9jf;Mo:G9[۫mdjmܑV+:TuZ<+}.{a#>b G%XH?c_I?MyYF1M18Mp`-z׍?=ؐy 3]q=9VsA- OfJ_# ع){쮇l:}one5㠉bJD83<+YKM=,rϼpS"SiF09Vهjgto#qiԄ\~aOi=Oߡ-3< =7M߉7?qԒcXL ݨ!j}'m8b )#햢 x݂ # _޿}t1-.7*`.Hgpwe<TG NiU'* 1J_Vȕ4LE7΢35 !bB(@+`,!>\ bCv=h /o#-$>P,ELiigwq+y/@G7kwx![BBHk+[ukeO`b=I3b)ze1Z[&LWbaN*W*c\BgZUZ17e@mnhjcq'G0#/F`5߶^:Mړ|ri㦑Fo>b uadKM@P+~b1el4!ouRL>0sdh<_=oFd. m֗4Og>^8^v|Nk/wj~;$^w=aM\ml{W#7::h1(qgĻ ȗ=Ӧ32ΫA:$LF e4zidm{qdCaW*v)ycjE "B9/ Pˈ>hnY}dX"iJNYʼti:O _3i"_Wۤt_X3U(m[q5EU]H1IQ(G8T~ܚ\m\3KA0/YL<5w6) (&{8xWc¿ ;qK1 rTyWѧcWSs1mvљmooV+F$JX#YWTmjLɏ h5suj#/9&|,h;DfԠD?C6bfNQǺ:60v;6fg#.Cǭڞk=GJ+SFDyvi/֎z'4VCs.(O,esq{܈k{^(wG>|8\ֺ\[D!;isa: o@dኣBnDoli%݉Coh]ne:WZcT3p((X&8D\]=wx=27h{68.l/uf~C&zh ~ zɹ= >i2RY;̲=W Q?%KԞ,yӢ ʻ2J6+,1$̵&{s EeHBKn^[atW^eE˸q3a?\t rJ;Q=Lkw[Y0n߳Ne~/m,uxdyiOM?A DY0,R^;BsAe]Lv6xAKZ{{Ƙl}}>@r%NJ5( /;&;e$#@dӘњOLa0+v*OV:8ck1 ^;0uwicgUf1DQPII~޿Xʑo[UUK=K\<|WUysPoF `ʧLyo1.}Jޗ]tcҸTIwUV)ٶլ:mC[ Z GKk҆B '0M 5'gGgCvЫX2n_IFcFFC!Pgz#lGz=cN<s:'TPH07.~  l~ulRt7~ }Rs{ œ`tbʆ;9z5mmqWP$s├]\ !o|JS~e9K5l@#dlcWn lһ)ɂm8'^-/ޣ(_ ch$]gw"2@=wM͎Wn'(s| 8];Ywɪd=Lgܓ݂*+D孎GjQ{Xʤ:57 ᰯ5s~ ̂i^wO8g!$ =QBt&"*mB\U{c!;ѠԈW*혊k7 㙆 .ҶxTÛ 4r)!xd^Selc;+Jz_'Twtѩx\-TTw/_"?O4ιk78\L6,03viÐno>es6ij"U-KuvTdWNx)LI'Yo3h{ K^ܲ)&xFt qǼAI;c}Q)I3>3qzqJC҈yu:6bE!dʬH"u[зEPDS uRT$&E ,HQywaBׇHW&BAr_!CiIssc;U+Y2zy9(mI:x2t:_7 9Z"zL;V%B{\ܫofp}ys 2d|(S=)Dw_`󤖺k^螗v:wMv}rA.Z_"bS;a~]x#NǑ޳ ޘe%mT$AXT!:qI# 3XՌn.$dKxT tXJƷnT2@TY6$[~u. "3X~wf"HϺHkX VFhkﱤΎ&c}uZ;zU-n(e{ ف}T.IQ,3YRDoP1>$ Ǵ>0ֈ3z 9_wU N:(5u̒^$|De̝]g.յ5&&DAd 9$V]!s)(WT?EyKp&OQ\.%& (tnz|X7(D'Df2dy8"OumE;Q]f\Q#wu:{w-r)$X8 sX,߀0(a읂,n.Tm۶m)Wuٶm۶mtVN̝c&}be\iTLeGҬh;kPAL(AnS00jʊKA̮ƥ"]&Ra zQN@Ǫ^+(fFz^bjS i viS fuft=Fx:T*=2NQI3>)򰤿[#HVU+PhmO-q_Ѭ҂Ԥm! i,)2aε:":{ tsy5)Vӱbq9r\نLٵtgN+cHOրl[@[φjwNl.ɆۚȚeI5߾vVNi+lsPSnfz[ ^!i@ #[>6EkD(,3(mXY2m>>Aa1%;TRrӪɶQ1"QyLWWN$x.k)VX9!Dǰ$ipgi̭ɏx DPq0I$_ BmIWq:GGMTN˵: U>ݲ5x+n&Zy_Se)IGHo zf5{[I lKHl<ƥD_6bafXdY_ϫBM 믢~X<+!hѱߑ4DD"h_9ͤl:b"/&x6H!v_q>:'TlK07_7@wb8 {;'˰ &$pϞ-8%RXЕVGL %z}!TRFV8}Q scnQJEsڃ֖Jɇ YTT+!h@!T9(@g$_*!~e {Ɲx&Xڤ13-+Lu{Skk✬JGnW\"z'#cbY|U鷾Z=~49p~];z:9>8 _aK 󞒜-'zK7yA7_ԡڡsPR/ KߢE^۵/U0 :$(:4&3;X*?ifb:1UC,4CÙRmaXR};ha`Um^Xs$BiY&M0'Pgj)")J!ϡdCaa$Jqi"S:oFu3wY"?;v'ճ꼄-^VJ`*wFԔ\pG sz*N7wMZWյ +^̱6DYK0U( wrtScmΝȒ*<f~*^_O6ԁeE/-)d-H楲5Yw|=;7}KlM8rjv{.~{ Mߎx+%uuZ]g'[5&w %3?Q2Pq_h:9q4\ %n8#~S_qrniד^U|ޕ7މ;ѣ/To-,<| sulGǖe*#-9~EX˜lw.^Wv YHڣ]%53ah)|/y[AK⻿:I?V\4~%o8%/@MF f"@!c3BiIgXZCqc@qg73MH$P+);UƇpkRȱ:vzmf6( $=3{l"kbبxOtkdgt {T) |B$Q~.ġJMeW<;HoO&b !ï?=yΒ"(Aڝ_>Mh41u%SWEچs k'Ξ/S7+]U0I ]e0JX3]u0K-k!#ƚ0"'CE0$eA= t4Ce09B`6c&Hq|"ŰKlLؗ5B(J,{ՔN%xs'aڍ˂9 '4՝5⃕^Q=)wcrQ_x[~ZIP?2D7L]/nڣ(m| z|A!tqC^My}a+> .u&pu.;?QhNg}g( teLc֚ VY[N/jph s"اwM2B^{`>͡so/ &e_k\~KuwuZ뤑+gwC~oHW٢P7cu{?OrDE?O`CuqdE] rlRƙoԇ2wuhx$.H%$S>C"Tz妲jCKŹGg.70dFCHȌ҇MdR͑_MXiS?O?HLpM7a<=e]GўL*Ǘ|{G&^oPjaXCb<)܋x͸MKAW2bKA iQSc0bftޗe> -cP0X\u<-d-u *. % e&d l3RxiP,x1rn(pel-OJtO!Q6% +C<B2h@ ,1ME{4jeo]xҭi3ݟuW~4-;|rBOx=OR: ,mJnE)vDj'04-@ͅ1}a\f&iPsq! M0\c; oz,bp㿗dH dUgR(i\zQZ1qדJElT=W^M!T ӧ_gfR]FȍG#?5kN.+ԣ;p[m" ˸[Rǽ鎁Z̤v1Xam#aP!C#!6Q$wdzX1Hxmk[xY'jRT""4+.FA&ŲS Gf_lYQv-΁cx^\R6w=zSx_/:eCx3ѿN;pZ| YLk(}_נ<'`zL|Q Z{AwCkNoZ%}QDBF+;rR#Wb;jǥ QJ>Zcڿ"}8G̍Am]YH_{alXcU䍝(q=o]z- SɭknP{-k[yB7Hsͽ!mK SדZ!ܵ0f!ؙآ9#(8nԶv$%Ƴ+kIj,;~ؙt@nGd8h3[ Z}UzJE"֒K s̢+1lO4k]\xqu$j2dLޛ-? d[hV\:`J+{{A~:rwK@si+F%B'-p abzP='JưuA@}ȸIw{!CsH!PCa3;BԽ-QLژM wKEj1Xg:5r~II#7~ ~(E5C'CiVq~.j-]HH<J  zsJ.Q\&)$ɂ|h#y*Ă&8V_̫)31信TV7p߫v0t!{m_ pFXtC{Ĉo$vfCL螞z&X3:: ܈ '] @" 2~֏(Z13)JY\eH췗-rq9>3@:;-L ?o20] # {)09lH2KОRYUf<:^m$;H^5Lx/&#Eãd~ í!icƶ}:{(ag LW $`0NɤJO H;1;>l.1dLYЭE{$E%-k'Jf1G}66H߉IǶL1)p8dQW aSdxQR*Q% "o8hd}6.'Çs֘8)Nk#aW e+WY*cVk}Gg&ZbԔ5WWhndߊ{patڑ^Zӎ%![(1'Bv6hZ8{qǘ$G[J.aCA {vc %FD 2ݥ Ж@ym y{2U^ȖLY@M9E+Jo%H L'xQ jAf,Tl3NqQ.fC<<*C=|cOF_U;UxIy{'.q|*I>cEGtÄ 9C7uC: z욢1eﶧﭭgKk`pJ'AQڇ@-T/ED Ddo"Ln!y&#jp2S  TAi{U}5>"7W 9LQ _ >JmYsBjf4sħf-;:u:SrRl é) /RPn:?k:Qtjha[&!G^Uqj6W^4ϑF3΢АE5y4*ӔQF\D|ϗ_cA[Ow-\Hr)-j9 3FM74+9ۣmءU_VK+i ҘI kڠQ6Q5.~խ# $KLYyYh7JҪ8B:ιwGhsZ; !8XKm-ӾA>g8T*BÓ?:>1չ%f*DaCF@H   dPe6=uݗ _M.QAX!D_2$DTײM]c;ڈ() pA3#WWmkoٛT>w>&ƢyQ-jy eBlP ߺH'h]`A;ca):(46REf+&'xbB,ΥDA?%ȨKu`R`KUr,kgΚՄ2٘Q:):RT81ZhMiC0WD$ê 0 IRVQA#C!wMf4/TZ HJ,2Vi~_};50V^tP,r5 -Iwåp=~o~z, +X0*oO}#'z224 4?%Prf7\r GzTcdrk@ u~qV FR`XN_lH$7  ޞ~5 |t@.pa>Y.E6E!ƨeko͛#MOF|&I@ؤ#j2gDZ?2b˵iFFp[q ّP"aO.}#oWU=h$NRbw]V!<5Ύ̀<6eNu!SEmeĚkl#pd1X/iU]GZG;֓؛T/{W|~ߚ]#m2bHb !3|̪vp~qVqquУh VT(xs& oC]Nt_QF.>mX١d&[HѤCª[UKR08nmSbmz%Ҏ^*p@ >N# ݫ'BoӬWF98p\n;ʍ!Z?_9/%$t2}ꎊ۠<x=AB+h$Q{Ugàv(;^E}EGZB@}Cs"O{pQUpGr;RN/ R#+LpnuE Taktv3D,ڏΦ_耙YE )uKް[lB; K= xWK@<Df0غΖğkjnqssM`R_XK w9J_\ ̸߉C@X$?-MAhmyYۙ?\s)gHZ.ea U8ʱv]H[$%,*ÊsHpSX]1չ0Tq) ɤ^u9KncGn3 hjnɲ)(}I'4kc u'ɏ滎Z$!3HLlm .M.3CQggQA4P'6/!= ѱv8@~"ŠQ?;$h`FHޑYԍlw5 Ztn-xKC  `hvv5* {n'S G.0c+V0,ˉW-1#|ZѪ $ɦZ5_}Q?4֪_U8n2Z'f 3{跈4)qʋkV^0&)Q.KE;t(B I LkxG1iOq[bHK -n{xCH|B:Cw 8S3agZvd7[1^N~߻`J 6c0ju(Oo0AK"5b``Xс KS0Pp7ziWylͬkt.ĸpY]7ii&Y9m(Iɵ:PsU@sB4h)tN:ξ9os'>c܉j]̴0&]‚m2.eFjp=IbX91{j+,4rEj>`tRN ΛΝrŒXt,iwǴoUV@r:Si'Ĵisc|uIa3+&b4wƢin%GiМC妹aS%GI FEi;q„\|%UFFairȪb-I!z=6,)mqB̬M]>*w€6l9IA 3EQO9sLht5qvI,2=Ix?ASx;[ 2µk,\ q`E{Z#E@"E[=ӍPD\qo[ 1FWOմ㾀ݣdy ä^{.\Z' LVfLj|S1"u߂PLqYS|m|h@ށ w}"?c@S5L)쁀f EWw2&nqM'enfcWwVRL>qRᕅ~f t-)ic_$*/ lƜC0(<U\qlX(ˠ(az-> "reu GkQ*㛠^u{JW^ty֐Ȝ%x8Xԑ0)lsg$r9ZpTő9eqm@Fm8|5S>*4AUOtRl\ZZXPiu6Z\\fۄ=7# 6赓,'2&#d_$ϕΑe Ҁg澫x/;6X`HLBhagS1z˃!|Ta[Jإ&<1#f]d-e }O*bz۰#}<Țfk?}ËK e61:r*Ev(חW^~xU"xu>mZ.̊ľջ*_3;3W<؝Gs{)! yMH8,FXc_uT I=&^$ֆ9f8'VE_iK\R|ZoMi霢E0ّ3ˍ CnYnQIHf*&SJUQHae\Rw QZ(1c e69[Xz59pٝ˝,혢7VWIZ73sqc;Dɺ$D8~$)P&/Õ工b|f=\."7Kh`e}(jЭ5Bf6+SepHa2mhȄEvʇkծ5Bh^1noQ SuoA蝾wg&3ʖT0vOC^xUhjaL*SO1 GY9A(=#VҠ-L<`ԯ3mPǬ:i+&Ҏz^3k/!r@ =԰ݡ4'y1bg?,~2$O}33lAXVv*tm"*/wC ~JWT= jڤ"jDvj0G+8QuU0oC qƈr$@l= |}rbː 5+to*V1I\wv;E٨*l_hZZ4Dˣgs,T?$m\z t#_Ǖ5EqPI{$LqW$-)qzmRXs'.Q@9 iR3#ye*.[K@Yӭ)0|>K̊+}ސH >~MR~M[uY9&AҋZMK`<˜Wϣma+`:Rs\'o܈R6daB~: ]Mj<3Qm >{x:-3Ux`x9c7;cXć [};A'GYmIJ7.:>G'D3Fky?U'E3:V"OTz8W'g[\Ƿ,(hg4 K<&6v],N=kw_Fx?HtP~Ð4ʨ$VEqݲrjn21^uleOps228M &+gtIڔ J>JF%y!ː:bDa~WR}9ЀGc-#~%;Q;f#8c,U:&\ԖgI"i5rRBW7ɭB5CNJ\[;ȸ6D7]FUӠcU9Q:[GO銉S"JȐwhf ҫ w3kKr.#\QI!:t e(cFq WGAU\TlL%X$DD}( A ¬Zf֪cR&V71ϴEcr㚂rmA?߲) ˡi;w*[Ri;2iWRƘa<;nOhN"ӝ6+oOf9)d=&@:ܦ,P=Br`T5Ō k˶BWk%*ךU)w(se>E4Va@9 ŭz/ـ菮mN5Ƶ3ސk#Q1ȃgU OgAoc> xq3Y"ZE2pP"r1d(mG&%ʠ?Ļes¡ٴ,f*D@Hqe{x흴78Q/H >9 xeSc#OE=kdA<T=>N^s\TVo^([?'W'$C+tS7uҕmE^7GAsY(ӕP.f~t?-9c꡷D5K]ʳkwcy/ lU;='eDUH%(# ,+|d fgd SmWNٶmUl۶m۶moά5z˓猕;O`Ѿ5lJ D Ų 'Y ʸuQ ܈<Ĺ4_{^3FÃ.KΞ& aWϤhkg+y-9%3P$]nK #H`0/?ݩS=7I|TSvxHUy GPKP2[qKNFhG3e)] ˰/1,V]u:HjI0;tl/ ӗ !lFfO5 O-ddB+{%j'A-Dzg]+XIكt Za!&\V*gJlHQ/ŗA4€X֘!nNϣCalҌ'7lȹUo-F'J='yC*SLNȞ>S*.}ƁuӞmAl`~7Դ»>7Y|o28"򊽢  Jo w>%^Y>e"yxLl@j~5&l{} 4 ~F%,8.DL@L+M0|g@vV<nnq_o>Q `7,!Ű6ǚهʛA#Y1W .ۊPW ]̿7d+,xP>yd;w.u r'/ےiFD7k.x Bq"Zοc!"?踆iБ4F jF kKTE[{: s.Vx?\}UM|TJX޸>ޅ%HtAo-n~gYuEwg_[|/x82x {"TlF C}Ʃ6$b*\K8r9M8E͜U3LOeOOi/OTmUS?{zftY&ۈR*)ChMu㎺P Db:*Tmfw04^]@HJ#Myd43`Kc.,uI3wsƙaBa`P )$J2-r_UH_Hpol] ,Ѣ3uvJջA:!KŴw^%oD!pY0sE8>ŷ6ГfͺI#{:ka\ᖿ5]e]3ÝVYWLE#|LGVfb]d<ݐ;fpŝ짐/8ߒo |Oណ!0%D ;GPZx=ݬPDlS0~;5D?S7vЉ᝿-\/!nALwMgE]m L|dl 'n[ &[o٧qS$I6`Ai#8 Gw4G ’1:JguԫڙS7ew+C씸韗q9i)w0ojd րilx(~OWk ֘~3ӔU\GW +SK[l#侮pybcZ:\:[=_`R&rm'FR/ vz5& 2t1ӶF"e2a4IjXY[wI]ʾ^dMdCL8,DЉ2TTx&0*@慉BKx  {>"v`d®w#UM`[/ĵccJw{PHV}G"Tny\RULLU 6 =pu\kI-êt5Ѳ|GHMA6I?ޯHyTkJwa8ƒN.TU:58o]/\62 fq.Mf;jMGMS r7P!.nդ}&זu+iYKzr9ƆɃD -?Ա Uyއ3j u.4wlp8p:lnS2$F~#r BdR_Dr HBq `.d8IN-1BDf(XPnk{RA 񱔨ҽ YYÊ`x&KsyN *9bNsaN BsaTzyE6q,E p䵷oZ?>g[Z(X|}Zo\AA;[ni4>O+0 LAd-Iد Ųy>/Qcϯh+$`5Qv& )a߾]xK܉F|#Jr=D:9b_l{|=#rIW5u%f %}ܠ),ƘG:yu>Tz@RiBp. 8kPnx>b\w!ᙔP &af [SgEN (6 f% 1 gZҠ8@b"GjEQƜ_HB8*$֓Ĭ¢nER~g̑}*ؒϞBӛ[lQ\"%̎ךG0-NsxAN'( yIOZ@1̗H #"9[ _ڧy@b{;KfVkK'=%89|7ka: |2C`| h =rl9gӺtG8y{pTLGh}uPT?|+"$!q",QǸ.pskĹ?Ct6lz|n3o^ߛA<[ꘃl/aVcTv`5᪍164,''$WmJ'ҙ}}-n>}(p+1&s/zW̴?5wdGvӪ\oO3ȞV!Zr!XT}H4=-YIH0;*&:p`~{fEcC's[!cꑭ(CFN`OE`GXGx Iy N6K4)ՔV/\F(hYե!RڵnA6Fy[mzմy0 ('4ȡVJ!]KyA2E/)6K)B<)[:;9tFw0LR1.#9Ch5v[XG6m02P1҉.0M>}>rΎ-*)9ȏ^_=%Ѽ1Rvh&QwtN[cޡU.(2/( ּwgy~\Rc״KRL"X(\@dH h! VҡfzOpnS#v9Ϊv6n:Drp6{²P#]; r tYY{}!(@"_VI&;ݗJ ?B:7Or%)<1'9@v EhN:=g6!a0JM]g~!kior oIN<*3wӷEro>3owWO;{'`$"w@>e&VT2-=0KA]؅Jh=Z"/̝;X,'" 9ludU;=en=`;cX>1lj] w~vLJ4q nӯJŴۨ8ʚXν !R>m}[iYMXޙy@z{ly]- *ƆǬjx|Cl`>>kkO^_U.Kz'IJ wGMGWnT](ERN2ECccwrKHͧ\$ a=3R,pIg ,BʼnjYYox`n(ǃL|ɫ lFq[]K WcwqS)yV"|#{xS_~YC qvUݔien|xYr3u}Ύjێ;؊qq">X:_ux<_mz\^.8=loU3@Bxy#\5C³Fz]">ݳ\B.KlŸ,ͬ&ztw|_e&gwexƠ9Irݍ1wz?uq΢YށHZ@oI=qSAƧaƧ:ZLI#/r]صf0q @:N:5בk!;8.̽Ї9r OG@ SG>{e&=pXNtS#"2t SRG>]ahtNR!4#`4;2 ߜ5זu؈ B*05qRoۇM[GKhFU*Dp20]4#`g +}f3u T-%5٦iƠ2(uKF_Ey4i,_i ve2m% }ug)\+٪E!'Ӵ}NIZ),qLQ+lO7pš>tkHg&(/АWhO;kiނ%w!wPp{n 7{ࢧtnZ #8)yRM%8 "!eab )4zghГj#UYExĺ _J*sBߌD=:hxT6?‡ôECJ)/pFvlwE)G@A>9* eC>2`Atu(0&XL"L.ԺLi&e&Mzܥ~?@Ki:8݌wC><>֨+5IoH=N^}tl *47lt%"hP?+cSapQftlk zl$ZtjHjMFHfHlYVWU.>[3:*׾)IH4#Z~΍H+s[FY / (nEᒧiupXu>̵-X}ʲ،𺶔/֚H˷koT.ڻa[?]9&Hf'b"'/q_ [?'qIqղ(Y%K΃ %12MB+S6oҀB[&,Փ*_j_ZhjJh:~J "Q:9sw9"*KFߗ|zC~nmQ@njcEf҅d3CGmn\> ņD뵮)NG(CYoRv kv~t֋XOq;"6G VQus<>6FaYK6ũ5J 6z¡a>I>>ַYG7t31{q*UcwNo\I1s,\WIEM jR4(xM%º!kTN FMJ#bgmh+[|8$ԗIX^n(|Fxfu= ̴ \S]4nwG3砫Y!;DKm8_ f-$gDw(Q +pp*m4Ty̳` geTR޾s~ G.T۩ˆ\ h=Wc/(G.V>LRU5Cʊ]:2֕u x1|$0JQA'(~KE 2I'sOij˖VF E2k!2Z?bjgziQ0|rƭYj`M?I#"I1u=P lz\/=&EOBOv?s&u2>܌dF69+z{,T@t^ A#ClNP}%")vHtP>ƳwL|Z0} ꟔ƀnbc Hb~PQ\8K.`WoRV, \:SP:E&h452N|UumҺKF4;o}VhWDJ.Ru+ crلmKzX\} X^l!1aX|".$'O9Fql~|MmsZHqD'>MN+n|Lo$KFm]R.@C0skS8W"/pυyp7`pPFq[ #E o-8C\dS V'=HS;l\.T[y럘-T/fH(['dj[p_S$Np+@J: 3x#5rۤH[anp(c6ȉSO50X~L3"л0z09a~GE ]2]/ѐP=BD@eZxO WS" N ಃ LF4-IoLEɋgU džH*( 朦=}\k$f=6> $&ɟTc_Y$/"w7MYrR𬑴qm- X!uUQѬ?z>.|xwNJBùlq+5ȕx]<F\)|yäz_Gb~}gKY6@]Ce-1" }Q2-Ĉg2d'&6R /TEZ<`cM0n]_UO wCg+sK?m)E, C(״xnOK[F$ qUU]Lteb\^>x~IWĻK[c"6mؐ^vqqĪ_-Y~}up>P}Fɘ+zW ~'Ta£`sS)&8R`SϷ:_Ѭ~ue1)CqݚlCUgEm C;OZj5@-[\bkU1F dTITcEG7a?Ƕ/kzZ;=.MֆT J_v*;nMVT<ɩ"WbS?0A* ŪP!SwtHoM!2 ) F0L^`);lJ>0Vd3ܵr{ߓ丹uHzE ::)\/&UgO3>$ܕ!}V(Tc:F)R'CV֋W v֓AS3mjC@ґa^C g о#PArɄMoOzagk`͚rgh{jKݱn F?HA6OJ܍)F14h%+Sm]xo3xs .>;f,/o},gUy]}aQ.0sU܋h#0HeR8O|ʣP ̀PN]~Oe4NsPթ4NnAX!LXCF_Nqk +'V_ .>] /~B]׍F^x19{X%nBR0m@ٴleȂxrʀёooߵܧ+9'w}ϗ\aBgwTOA4^.ͺk.q8}\x!OB lu[iv4ށ\[:sA`BĤq[Z wc:aNM ʊ(c>H߳b^aV/NU!U_ 3ٚrҒjV1TvsQ( C1ΔY,ZIwIn_ ǰgܙL'=}ABYeِ56㢴kfI*lߌf?yd cU \P(vf;%sD_;D}ZSVW5*ȴ;e+7-l0%2Iӡ:F[|=X`Wt1k{Τ2'9şa_XV} u+du8骞N ?]tTF덱r04r+b oSD盷@Ù[NVk[|kw;t^0+\*nБ"PWe%|nk ko+uǩ.>]ѡ9xP ]feMּ c%=4i=<~#B{oq|c2ddO~@] L[L=˟o*44oSpZ &8""˴*.`7 ,nҴh }zBI3 &4[hH)W6:m\6q1*#ۋ>ݶ>= 'xHC:^ 3F[d5-,fNWz+KrJ5vN^b=M(&Y]+#F$bJ-?ك0ނCi"dݧ}ut^ZhӵL8N-rOQi}D0n ⎘t7xv|;cvg=*8"vsұ0 iL7kW W-MʀG{D~fFk]L28zvA榠E@jA`~㘖5gRchJ9Y?_dģ %Y$ 3 ciʚK6 I fo̺-xŐQģ޹nqx/#oӚ^)l7[R]v_p;:G'*:T,nX3cՉZYeZd-CEvMrk` Ϥټq{>@kO*"Zm% 9^_3*;59 _L"EMZE].A-f l@We 9@ڿ00q7y|k~3^LV9z2UI/ ^O ͐Q1)l{*OyvT Ff1ʸ͓P*#Q5t##waȤgY1,F9ԶdAM]cxlhvGuA gQid,'b^VEni1D^06FMT8z7QDeK|@jO~NL'f(xeoº^=Coz," >$iU^Q7}@٢uǢ6( b佒B}PG#K3!NfqO^K)8Ft.OOq>b;'} a'@R!88`x9|轳'Tj1h[G МtV+ʿv]+븂${u -E|G???uP6í.vNHoU}nVoxY 'tF c9yG`|E4~l1"q%u3)]\1?Q-|~W:~P2v=ʮ?qh=ā9}`<.[i\نs=W@=Gf^J8nλl$T0<~|C.kw4vO}yʭHBw"\W/yZ'S U1_I $őbcZ~cRijQUys󫒓%4bHN'54-+F]\^\Mա3*g\,mst9| |.[ɢ:#ZLpnP;]~+^IZƶ\2!R#dòU{P+ASY`lUE\}w#Ir2 ;%vr=$hJ\*rL5A/oI,gv5oC!7!FNjC/98M:mfc۶ݱݱm|cmv~{:g[sNgճkc1ԗ-S#:O+1@q2G =K/; ,)H1%D $c*g.|@n +ﲠԓak1ŃNs&jb 06 2r\X ) v"A[q):60:y `ϧyJSH^XMTRT0-χ%UDPj aݐso&l >äAWhz"DCfM0Cb-GR|%4Kɲz^M"*&h53p-pmH% ;Rݟ{{΂iaCjWVΉqz>oaE 0Nldv t<_B0Y.khgDOa/X3oaؤX6+_A^r2Ai9caD|CU%6M/J^a&jaD]e0PVM=y05!/>V#٪ڨ)ٍ5f>V`ly?0286D\M<_ h8.CPM8=^ݴ _Ny}4瞽#VX;*ҤXXl(u<'܇M'BR@_I';)I5Ǵ +8dwkWQ@r 3J;3d; ͜އPN:A}'2@=*=nՃۇ3/c5*3klKI@2/jpoktSrW!A5 bOaUp@ c78JY`%$!3Ӯ]^::#ZX@(J%꿐(uJ 1?(:2-.\X|3NKlkLrx%/ l <^(פ63.HUlMo Sf1u,.}^#n{|:C)"O Z@~ϟu]6f'>zV5ñZ{JU G%f=1rmF^)BvVðuNEˀA\.^P[hκR/zJ 񉐕c%]OftlzOO^o@t%D#h( :ƪox^Įy [x/2g['t['s3rp1h7O/'/IƆ:UM]&ZHƿ^3kf E;tGήS,$!Biz J`.1 :mVROniʓl@v젠`2v2јQn)aCObI) *a=*ckJgdJtqQ.s O5ffaמ>%N iՉy7MXxW&sCl2D3g'$0&e9K8mgጿqA xV<"1>U&{8^wh-s"YH(X4XH4qYgN*Dw`Z1_SLZ!M@ x K+dako:Gx{ %yN=iOI`sْ=lJh7@gZ"2f/h7D ͒N/`P. Т{ViHIh0dEYJo*6(]RN9.4o,=BGqڒY賕8%>\*3XyPI"rˈ),Y/HHOsF4Wm,df'!=-,҂P]J#XG!K'BK"GISMkmdW~ɠ",jӵ\oUh;U}2%śқĹOjCiM'(L,8%OLj;g+!O]6rD* t4(q̏"闺VeTy. hєBmCIL7CiQ{0x/7vOo y]M?3* u{ӣh{XG`sBZ";`>,#l)Q tϒ"BA.lO  oѴ3aVYNGHZU(6b+}f\**2*TN!ƻOۮ:u _vædY1.?6U6D ~防B׆U ,%9+bM{t6)wZUFt&,]$(?4\lh _<`,#"3.B '8QJBL*qh,B0CXKɥ6!ޤc%g:h90T>kp6.1RDؘi2P[P*M&yCK LOd+Al̖Ϻ%1 B9\H2rA 7\\m($ ~qu-1vmh_cNo-،=j=%xYp>9|e!,^7Si;EbC^@H2ba de kK62KǶ2˼^=Z=Y;ZU7Ķ3ee ?fۢ(jGe@iZv#\^ /I&GKˠ#|.cήe/+p06LKFV8< ow"F苰延\2zUu9'|#>=|s;@MGtm77BOK5m=QQi[b!΀{.o*G㋅2U)PedUMȫltE4LSlH`'_] 3,ZGsop頳O/idxAIM&60/(V˟_,lxFsa8n[؍҉%`w9~3c0 gaM"&Yr$#]$s=ju^Om"m 0TK֌9Bz #̐-Om6vPíա:s2֧KnaaكK^:ZD孱ԭGR0Ψ0(']ȶ dؠi4^lsJ''Xm'~!g K~ xJav30_r!4zCf%zKWN?b` ̀UO;UoUs^:V-FM2B2G(ٙto5m}vPm'RgJxn-|MScs4ONsst8+AwJד^/Rxz9k#o\9ߔ'snhŊ}bYyE`x*ٰk\se:@a]m: e/,˩JyK:,cJϻ>QT]Tfk4 M/[o;(Gkv\{i/B{Q!v*b0u(~\;Iq܊E*yJo휉Ǖ394NHЗpoؿCD r]W]+{Wt Hicsܬܰ ֽO]NFuU'{F@Kwzsն/uj́Ɔ&ղ GK ̣pdž~7XZ*asx\h3pOtc{/Ua_HwbOPtvKHLRR #͘ (e]c_VZSK4wncښiSCԁ>|th%v?QCK̏)qx3p̯b0, [Z)IĬ"k>kj:t'~bpLN7Eʲ\>{u~9|T\p\iqpLJ(7JkFaV?RCd`ز8 &m淛#G8"i`5BWdЗC(M,xfW;)ٺاȊzN; C{ʨIحwIV5;Aݖspu` a!K4Q?)2FXg 8IF|*d.bP|/mmnGPzAYdi.gH}EACцPZX?xZY=D#dM\.*()m|E\ĵhba@R22M)0K g׷[$ hᄙ]-{&L l(qddG4DnVuVkr4|J~!ٜlҵ O>L Y RN6|/[eZ,kEH*E"a+>i(l {yc]5[vdKwz_i2s#n;5Rpms(@˜3[8F\;To+s) P[GjfdYurFП+|o F&eWi u9U΢clA}EA|2"UĕwvBD=<>٥O_N4몞x=H8YS?>ЮeBP{2+D8 G*l$ߵˬǬ*ߘOoͰpyC$#2 -* v|vBCmV@VN΂]syjFo>}% 'jxK0HeD Q K޷|Yt8}PQn NVn'_y߫X<Y^ы3ʨw?9sGNhy.-qZ_ VHNqoc"LU͆`(jDIBY^ (el=`T!xSƨ o㨇c2Jqdϑ5( 4iE,) )iϙK`qUNɫ-S,mGS-u?[we]Ԩ? iבwD#jɊ3}=8%(FC6IZCۤS)fTCfyҎ#@WWW6E;vr񇬡h5va$m4{ٻNê#2:%ݪ[ a\˺[ .ah3[&rif,5hZ}=JoLx[#Ч2׵P75 u׫P=Oj{I.!Zs4j2ͬ@EYL>PA{窜{EYarB/n(g,e NBhJ;;-48Wd.,=rz$!/;cjBWA'狪#a!C&kr9qE{dWIUC$lҴW_߯..(ih l5Ķ!VNl$+xtޥ?w,uG0(`q| 38pk'yswcOnÐ`'ϪV_E<"!c  f]}B_a?"jD؝(¥D_#yOgeI76JCTR+W_Ƒievi$B:~(HJ1硄b4#pvvB~fӆv$ɈKX„,2',.ֳ{8P=NP..EØKG;&cL8]U 9vg%)Zvs뜾nEo l~ow= >|-DZvoKydQ*`_<1`j.|%=]tA~@N@$hwu)~ԮhPdia+aBԱ19GpVOr25DLQWRYIE/ZG-eeQ~-a{J{𣹱1 .S/"D%;SCmFm+PyOQ+r&=(XS#  *ۉ臅S2>?EP:#qBZ;X(cr?]Fj*yHȫ6BIG D+@)'_ɢ=Wh7[X3iZ  $~Ajk٣v ~dx"㎸ߓVaȫַ[{<W0Das>}ej~;{!P홟A6U1 lHW=q79t>PLOw8c%f9?G ;a0^Hp+!MнPA?v5za5n5r5܌{w [w:3bD Y  $n?"?4[p:_M` ;BUsf_RnLk]xJ{ YP !M |AAH_ir7 gLLLTJ;7{g&gQO3#H79j9fl,]B? \:_kMV/p,VNO7 3 ]d3jxtRg j7D4Q(տy>K/GQcs `n +;0RC \hSc]_"NdD_7/XC9QnQE&RPHm)*'I[pAңό\q3:DhP:PIjR= /i%x P:vA=y D0.|RQmr֞; .tw#nv8*GpZ`A>ULYZMS!GX _uO@b~e5ύ4=M`0#`("+W;tS8S+6dvhz._>u=$&_ z`gȝDVce\"+az!i襤'ݕ^Bm#3YC"6ii-9wRYJXq)/~T]N#M,(Ys\gi[ _dzPyӠ8&(Pvd Ӂڛ7SM.2[ɹY軣t%| OYR ]hR [UZ[9[{^'$)#t HJ˸^&آ]odQhg0ۼ~wbYAk@Kfhl(vp% wbBHw1 B:ٚb#ls :af` l VDu.Ks)"l\x6mxLwsٓn!]UsWP XϭH ? H[dfG^E1f*= e 1T"fىZnKi &),҃^Bv"(ݦ㟉rߙ"UӲJ^ffB8Wq*1ۧ"V ك%C cE2H?:l1}d tQsY)T̢0̈: 1Qq Vg^[4k8/6ˑafJ13Gc+b.[ڶD7GؾZ02pd ŧPUQT`eA /4V.fhF;[&:*׷ѣ"/UE_K?$>|r֗8f4.ulSψgm<$HRl Ń(QpVAuF;UFnXMbS)cFypWfX*0C̺6Pk쐫}dخH˒@t iۅɽ C "H)6(N"0B]xP<R23m灩@G17vYҌ43WrDkV8SkܲMt)T'ƵoDx5 }qܿf eP$AwVɓ>^65 laQ^0и'gYY{Ǵ7彳M}w.QC/ML{1WʎVȹ[߈6Nyو譸tlpy&4wm:6Zѻ~Euؕ#]s@VBoaW` a/ (+.l($Qp3{螠-~GhH,[-a-@_:Q#Gg8:8ͣ?lI YUgT|T 9,%3GVjrǔ.^ATm?Iޭ^upa4Rfo:Ctlc$Rcݦd.\@o<̲^ w-dJѰ%.DS:zu50?JLS+Va]l#|4i̔]nm:#$WB+[(XX/+2ʪHEšMOcf&yБ0 | \Wg$gQw˜j*>/<5҈薄j܇bRBB,#ŔQ~aGS6bsj6RC3% ؋&@id-}*ؓ?0犲!LDZ劽 迢wD~-5?Қ׊Ġri)W[sawHka4ABQ/8\xD kn1)7'"S2t`z?BL}/7 wJ`bm[Ri^>(ퟍE1JgZR{LjjvQ- ?XJoȎQP/;;;  4wV4BDqY2XwPq 8L/0}onh/R?ߎDoQԷP^VٞV Z dT lO*[3.Vtl *_L уMӿpx,1ss3㒒d( IYZ]Ң $(J<ݙ#e=Pt-= 3H:{+lG)>cmkmOe<*HX3t,V`d[ G~]N{-!uGݓ%F gfQKw.y΀.Cg}OÿbH͘ "a-?0+M bhi0(7,ԍjrS**2U="뎃0n\|Ԥ( nœ䒈o:RM q CjPE ʺzV aVpkX;׊gkSQw~ہnT3G .qӼ:c q(cvӵz HM3NQ$ w/03TBoEnPKY?J_ppN`T-&6R B>R̢@sRo-HVD=jժ-;;n"xt͓~M;>l=~𻘄0̓u ZV\gzn3v-Ϧ@K=;B[P" rkS5ۂuUp)F6%(j0Z-KSe 3SKZL:yFfaJMd-Lp3'R_mͣ ~L Y"5+֞ Si5<}`? `C_$X/ڏbOkd]]9l\nگIwd̻/# Vnc[<2!!D'v ɘT~cZLd*+o߲'Ϙ5Ycș[6JVRf5]xO=t Yf_K{5 *l:reNj xzl4=km@:xRRz`qC@.j}~ٚpLϬ; >KݦZŎ;/% IwFFqR`]6j] w$GLΗeC` *.ŧ_2tL;H HcIPj7+j4igK4 IxuX1s}> Ue;5ylf fZV_< ]oQT+wy d >ǢBɸY"$8EAMSM8g-U;KVTc T"5mzj?l VNT>l@czs[(/P+om0esBEJ\PIÔ娅Y]%B222& PU- QaX|@-֗k|״ull6ϵނ]DHKǫ]o}׼~<01# `K{,M;O].{a0dSs"qO+eltfp\4״ļdI eaKͅnk &\.iBO'3qlM,,h+Ի/aRQؐBر44VљhƐM`11O<uTtQ֓jFB3 `60'-= NJڤ4Dצ-tKO smtWaXl(a -rOyi@dN?q{pe3rښ蛾 !!*{pl!5 g|SO3O0mNp٣%3ٓ\Op}u{z!]B=:YL`^/ q\ü\Koe2#=p{{;ldGr1g &Q[3`BxDR1w(l䃈 V/to0Iyivh(@n@9 Z/q*ʹ}O3anK]7Ϳ6ҫX6&킗; dAOk)MJ+ xxqF>%~0-1Qi0Dȍ:MKc%hܦV0{]PCRu;zU9ႳvD$Hmy&,/'l= vO+%xM rq8f$1LM7&X,2tG58"Bawf۲f!OJLJ3'0/ YiyxS^K;hj~»4]#;n贙﫢!STg^'*=a~l+'Y5yP6y Krs6e4i)#3:-E, *=WN5Lnŝ[x TW5Ħ~ Df4 B:+L 9Xaa$pQW:g}E H8 [$\TmTYva8 6Ű9*70+?tYl}{@'iO{~tf6^d>}MJIn„X}4&ޱ cofIu0VH6Ik_ھmÕXWja zgLѢ1DK]wcwK*amϴċjwtk9 1Ս):g&y=^**K&4^D0Q{3JMOTKo-_:'f*_=} 6nK(GJKR6mJ(~lR+ ay`KV b^-=m*'Pu oW@6B˻sJa$Su@_RcUt1亨`W&jxZS@ȵNT%!"da"Cޙ&"_@Bʜ??IvKo- Qm~AF5z'9 =ᅮ$CbI}7eHNoe'Ŕ:}}F^{^Xf#\"BbCXfC7 Pn&ۘ/aH._y7K?/aKIqzK['Ml2e \$1g~n9nI)ٱ%&|ӻW3܊pƍ$EYʅU;7EtNYw$(&DRYePYpZg-CQd@5m vO ӕؕrQ'ryL;j\$c 0LeȋFӏL*?yv}ZTˑRn ̣I$#b*TU ߇M4UQ/\2Kr5I`5;pjj@OXDK~ax $ Ar z'ԃmOzmƵLo7}}yu$II{ixOoqV"<Ę hĘe =聇6-{Կ~7Vj-4\x8pFJ͝ ffXl+, Gv <LNW(!KTq]Ev^뗓ZQ Ջ(\FYcfZ>,tI66LFs~QS4HD{06E?4ne}0ntEVev<﫱QmHY6cZJã7xhp_ύ\+r޾QA >sLS3C:ebYMnCnCAoTS/ǔKVhuӷf\mZ̋ۆ84↠ 3x.yܴMQ5VQE yU l"6DV vBO@q?K`.VKׯ`_]dd>O&@F dHVx^_&BW3xB 71[{ yhSX#_Zأe8Wda+ue*=VPzG2hM!}+vŕSr&|Vn\G!XE/~I"#6 هUzWzBqޓ־>隿إsϛZ%2S 4ֶ]b1 GX]“ M|[16ďh>ƒhtVl_|uR  *N砄.eo_\ji3U{/aƕY<~"0!XJN iܳTi{ЩI<=Bj߾ ! JGpx_˝x#/;9΁_V!!f$1~Rׯ%.˻ooߢ j(^QZبliidYǠrrt3f9RVO Ko"W˲GGkNra/ ҘHNhzjZw]3/J np|x^ ǩ*g/[b%tt!Y95fa8SnE ~7%ޔ;1ʘh-Mk[d`0CW5(e$Ž΋@_^: #5I W3azHu%Nd0yD|Kk8,&cw!" ϸ \\ Z٤A3m˜a-l Jmz#LqGh]uuCw(^ Ny?jZGn:~\٥ʿӠV۟{\p88^=r@b8z1 ^<;6IKɳ5y'q hǤ#*ЃHd@ B !=$X]߲Z6@WÉ=7m}v_ RR4{IiɁ+GI& ަ1#P7T/=gAji~"p$ ϣVȀ5,}ɤZP쌋İc2!kE PrB";̈́b)AW"[dXėr{ YXRqFf%p@ yj|m7澕Iu>͇ $S-@f@],>7õ4M4Wenh{.FbܡJ.]˅'[txtRo.H=e:cbIybJ%Ѕ >Ww,o:%sy'yZF}2uHxZNAp׏ѶP2*q߃ʰ]lL62T 9Hr@)~6OԪSqnnհ=rx֮ml@w/:lB=,oDMTP="zKt-ϯg *SBtb.'1mP|XHIHa/3T]5p8nqwb+I߇O5)Ƈ1'cHs♲&3FV÷p04|]*P9$vy`7Brm&n\٥STTRi9mwa)ümyc} hZR PK+RR쫩XW"bю+t'){[{A "0 x)nOlpT+ZhU[L7Dݖ ݠ` YLNբ]G3ŦM@C:"n!&J'i!M{>֞·UZGH~Ӻ"NykqO`v v#`#O)7~yXbA I|"Piv=1?@Uq `s6w$'4`r9hQelHJ_Do޿*n͋#WPd'mS^'VI)g+&dGSRHKdFػlln.mLMI»!E.e?%OMշ12#KG.ިyS5ni0sޞC:NX}2(:t(5,:iv1eнt ۲FJ e%̨W3$VU\#%8èܲ$w>;&(_ULkϰ{lecJv=pD{Cɬf}ߞk蝓tOiLzM`v`U:evd+TѬ)wek5&bDkfJ7wf+XzB# DEy*+aSr5-?|knxZjY+5:\Z+ ]([ҌWsB[~u>^+7@\˰3 oﮈM1_ V>, oԁUf_~e3^N Q9s$VZϰ:$\ɄS:ݘl 0)6!| rr(@|JY.2ε x؉hoHnICv ]X}.2[f}<7u]g cERۮɹ6 ]ͱMLg>-h.4_F7awLj ;MPb;g~$'& s֮ !/kf@($}bиêhZ8ۧ|s;܋9ukW>naHaap4HF f{Uf'kݟdEJ44^[:,ŠQ*`>_OKsmmxG:=/xZ]knG m d:z+X%ow{edyѯ<HbQ7H<3$-.%N ЩqidĞͅPnTU VE)EfiZAB1+| @>lqG?cQ'>+" ,WBYnoF{dX\`z /=ժŸfnKѐi׹c!/'tXiӶӦ65q =qD /hU[5un|CVXAJ wy)^yIU#IJq6ma؂3RjSשbYS?`fyH ~ZC(%_n 0}!iQnT gj2.YQ1\Į-zoʨIT:P4Oz̠SL1ެSҷ|$ Y i2^jRɣr_`TemL]:o W?["*EBo[ф2Nu} 3Iw-\v7 S\,pJWyJv^ Iuym7xјpkLc;h< #kr.tzʖލ,BQݴ}ե[YYy#73NIݫ;s3qq"'|+ ?WGP";a,V::T\vy4e-tFmF1gS4px H9iڲǟt40TiFolro%z]m6f42'=JԶaeдH? sF&C׌]pƀ{&P;E5;Xø2̗1VƄߕUC?88$A2ĩ$)Ѝb8ȣZ3H.GnYy&!e(4x[m*mUfY_&5ΪHLzNڠ7sZ~[cǽM&^U=GGfWdn۠4 7X(֞T3nX\(P{3PDpa68;g(drS:P{ j.虄yik1Y l"'l%H#v(qzrQ ;h*d֝͊b!2U UW;*CavQIC8kf0u18S8H yRiOmfk%x%O#qm$Ɵ'kH}\wHA&wG[حe5+$2<i;}U& /W 2=rBEV&m=RGv\UMٽ{I- ѻh\ؚgS&Q.8'9 SO5bt7[o& iݾ11ܨLJ4fpieAՄOȰF=r/ x>{"[?)EjrӷW`WP!#jɀ>!;27>KRst }0چ֘qUZ'r4qC<|pݣM/Inj2EB/U@b[PDPӀ[h8ƲxlUxS)xÇK\h2ajѽ?xSPSJ.ggzW?G8ES/C.E4DG*+ZdKf%N8WMn Ď$_sw_nbx#LG󵠮.힓?pZ$&|R*n%d+w:{t0R?0G9ʹ^Ŗ 3pqF_=g&^R$~_@~_:Pj@%뤺[VAM#Hu d"=nsn/w! /1ئ.ހ|J;N$S-9<ǬLqTHs+ iZOgc;bS\rV=wlr;v"a&l'/Y0Mٮ;X #DOp?TCj2TO/=χõI?0rCt ',pEe0RO_~WθA7`%QՃp^{Wb|?^<4$6'"`xف3r}At߯O*h]kT%WM0Jx.i۸U玄t uh-!60RTlM\\1SHW}׈ǰ&3+Q@Z(U%V9L5ү㡳m|Uwߕb;bWD@O;?{tcw,ms:V+xoj從p*_7Neꋞw9jPMǣ8>f35L$.K$V hJ1Ҋ;I鏚ЀY JnxYVko3C\Qn'PA(p{s*wyuo^Ԅ]n-9χP ʲD(de%IK} aO36i Q@58xQZQ$9Ҥ6J/ AYB#! |<$,ϐSJOIй_ aaK,t:1LDewPy7:ȽFŽ߬hBvҽs""){'z{LllzN5q|ndVoI+@5hҟxB6uS10#1 I(9xpC/w*=!G邱z 1 a|T9"X&3NHB2y|{<.EK\I)1_<:xys.X1G9>`bGÕ9SwC>vlYDKzt_IaR{ݒǍD?\} C,]CH7DvZ3TIG 6Sb7kB-7ټ\!JX`n"!+:Dٳ+K5!#<kwAdWG\03ïQ/$8([,%#IRF]mR>;48sW±V.ˇ?8 >s|X,G\fM'/@n祑$ϳ b߳qpeZ+n k4Rƅ{K3ubN : Kw+ C.X98d!{{*c(Ŀ4R'_/ UY{E mg\3=)5%eJI.m[&tR:NBu=s0H;|,1̬*_ -;e_O]qPdQP%m_V2h˚y^pQ| |.9!o`ã&fQ܈X1أ613bN{EbыN/M.~4~}cWB~J׺_]:I*CέZpYwyt&;W.[O( $9ErUy^Vg6l>Zi.ty;Vd Zɩ2؂'sx/Hefa< >_S=/&ּ̿p*-^!N6|M"#3!:2UgH'!-.}5SkCĠ S#/9V]+uy|oH3w;y?1mw<;;.=uW䒽(3g~F_mm>OX?Yv\OᅵN6naPn.?$,`oAtj'a[yd©t <:ZX`1 4ʽk~ /=rlRg,/7Uq+ qG<,pzpF ,EB|v] 砾Ƒ酗Y/$Vg{~.dXVPpgMZ47D/\ /wa aDzy+L6 e&!Ax|)к*E~u埑LD rhHt9d(An>-n|6h( + 2->5Xɟ3丽ǹ.7'[^Nc._ мy4߶qڶm۶mm۶m۶m{TUEFTͯ;7sB~8qgnkEߨo6S'tߢ_hiW X&~^Ѯ6WGNצxɋ'o? 3m3|̥wzPk9|\ټ36%lV˹T?6ZG?MX1$g9z(L\MKBKi=2 %&AwUS}݁:f'1"TE`o!Ep3~Ukg?dOB?'<\As=^hdqJ#Xgu懟_VHw߽j'ټ!?YRh1TK4dNdt*FZb˲]t쮆2mU%8|Xn$`MˑT4ʑOY]Ter0IM sȠfvQ^_;)1I$.&aƽ5y^:9j%tY(hgNr#6o[.r[|sB&DzFy53u&9@{l=So;;^tǒ>- t .uw:4V<*ЕDgM.;NnH.u6\Z'bg n |\9)cRs^3 #>i<ӣkmQ"r[LڲOPܣoi}Zh? 2vnUwc'CMxkr!ߚ>x>^3 $!YyQaYt?; ǥ/Bc( J SQB`'F1Ӌʥ" 0')fQ[ bmAD4 H#/lJuY0j8/[4kJ@#콖^*TqG[bhsQxߚ; tm(m9SXCxL;lmgGJ?m";ޑgEwp ]B5*%<%WvYčja["WxlB[)KtGXQinVL qG_a>AB;,B?N\|yHʶ߻5#/aE۝88 KA&-2?s;A~8UhQj2{:_ƴ}^Ӎe9,ɋf梥4 h_g!4 [U-M6)"МD/6>PַFn)2N Bk) [fͷ1F$e$ulq='{ʤZglY.Yp:NbݪtsvsN2/q&G#.Hgyqbno2VE\YU%3J Y08z=,ėF[ϡh1)&-}aMG5,p6Ӿ4lvbmq($jz.HPOOE ?reY ͋S'[Z~hA0OQf'f 6數S*Շ C<+'0))% eOmd`k2a4=a^3%"47 'em RZR3Ƀ@B(jrfq^lqHh"iIZ}ɇBwqECHIؗrX[1Aq M DTQM÷EJhE79D~+\VRSrKӁH:7mSUQG1f33JIRY1j"A<0j5w#9JS0Kgs0=_!)dV9{ e*rSt^6^ńPJ׋oc%ZNFO?xot:fp(鶉P>T GNH"m 7.l&R# 6W޴,IHԦdvpګf-P{j6#~X4p&IÞW/XtH3bՅxѻw{ѹ|mOY?7VgRg_|:HScX-s8hn[ظ=~ke^rf["ey)gZiS`fȆmPA۔3bkf ׄMQɪ'3H WcAV+L(5{96jk2Rw{QبHvБ@>r9X[8*Tبl%خ4GۣM_ۮ%A:EIJM,td6%gL$B TBC4ś))sQt>4JFzڢdtP;O$LjxƱf$}&LS=o>Xg__8ti*aZI\еs4|0%{J|( pM^-zI=_|+pb\ss&WIt"_߻Y#dߓ-~ K`ϴ/㜐xTNzxY tH,/apwa))klgر_av ML5l52R >VՕϟ( L1zyHc7NR)W+T@>|!~ӓ)QNWMZ!Q|J_Y#(Sy %KʰݟSQ}2L 9Yb=&RS*i. )+3e;Gs&RM- o3jOl肔\yMRˣbgIBt*^LC '^֢-!P.2|A +:arraye|VQc~^Zt9bH Xeŏ|cpMr%.;B[_ 1ݗz%Q䝴\;ۚyex,C,W@= LoDcjd/Y>PSolYlעYzMxsI7.Ȫ}:5:z!a,(6TX@hTlx|=Тm"EjD |-=(P"͆, nbgR]p 1uAஊ☱i y[钥dqW'0= PaY# Q2cfB^"QÒgDVB9Pfi !JW)%J2!X#_#uͩBޜFZ=*!hGGm8gg#hTmiTTasX#SD`$IIb(ʽ g@ܬAӝyQOH7p |(t‚oJ9hd,Ҍ PQB],C1X`S3vI-\d*62 Zb}ҠE23J2.!)9λ-Z H͍R=vܙ9J˟HvlEښ Ry"42֧ qlRsڣ|/ eo`59p_N`4Abj_ 3ř$[vIs™Hu&-EkX^`8~90|,b4ɨ}a&YCW\OwXv*٩zÓce"fnϷraCnŦrE-%#xv߽*բyUr+п~jEBʸbŹ:4!n^jY"Vу5LСwf>9 V$E,UCyX5=!7a}i*qB@eIK3 Alh<5p1уlx< jSu*GZ,IЃIPk=yg[4E%VPdžpY~429ͼ~̲y禍yqKcs|j9>5ZjD*ǔ$0Bd zi9ʶp',訤F)6QfuvӲ".tPq)9X^.{3tsS]G/wIo?[V}6"ގ]RسkI&I&*T{sɹD5>[ ŞC80->I<9~^Mf0Scq\'hBk]=z2{N1guuF}W׼piP# YNne/#xgVs9#KHNv^٪EN-q2q=rRx:彻弓zz:XdUd7h*Ȥ3:PkǹkМC-N%cw>Xhwy1eӁ{$kDHWZ19rT!kN8^qm.*>Yٵut-9{TלAvĿH#j.5'vNt⺏+eg Aq򑼽A%ݧf 6+[)|BG\ &> zHw'ܖ-ay.ʌ7@λR7&8zaSm}bs)2b9y#ś*tǤyS1.@dԃR  P@jLSS6:!;!&5MMR1R>S7bp-a, MH{åbKխGTFGGף]F[5͸?Qb틪=o]qB`0Սp:Ќto%\W%;`t.R@9u2[|<^;v}jb%>[_&Z;^1cBm|]6"p8¦{ÉDIDùM'&еt9TbV%HEU]FW e*Xj:b JzN31tƏYHsXׯ%?>^1ٳ5#Q-q'?XĪTQN68dwG;^gl7:"_#i|aj@:~ren z9ge~t*?mY^r؛\ǝ'qx Xjs)+;OROK@gg2љ\\iuHTOګ]^Y2hlso 9c%|m%IWr"ĔB7un4>J i)=_*kJ._+:R8ISmwZ>{\`|/]0zrr$ٚMv)D-p"\c`Zsp¥fՇԞ!"d9:J0aȕ>>dle f hJR&&9~^ꑕ:sѣ]W-A}P3anb ~AqtESFރ0v1cՎ)wQ2J1!Km:2wXil3x=`{B|fe+(ͳcF{B2vm1FAlSSd"ޮ'?6̰_Τ{]goths :'mF6d#J Fwa8FqXRn]7Qv_o/ŊuE8עv!'O-3S9T ;oMR \ ZI1<}`cϢZFBv~[%Mt(_]Cw-E@":US#MO,?5hzbnH,b9ݰC'00śb*_ts|Qv;\z.mZgp.&ȩkM:aqQ4^NJBb%%2$q!ߍ.r@y+//wC PF @C?=~+mz0&%<>߁Zu UmewfC!Wks5_fy6qu9߆l 9sGG.j?/C$.zB`ft-!-i9XDA prqS!Q]~jP L,I{,e66C4Â`#$QeR_m ?SM۬E ^ %vNdg9*r(  F Ifqpc?$ :ad#gI9xLBR\RϪpymU#1uι#sO*bh9^`+4ѹ-]7K?^HCmCw(|(BX#k&Ucs-}7\`8;_mV9L>וÄUɂ szعU+}6]ĖQW4!6r!ؑ_*U<MµzO9|vW= iv?,*ol}+$F!RQvz\ Ӌ8'ISYaا¿Q\DM#gxy7l+$06(#B.ZfZ E<'cԔEhX9dg\eD@PDxb[sHĝ$#W~z%Rq͜~&N|DD#CZLyZ@c jMQFtW$ګ!֛ [_ykmoտ>l -P[ֱX_#4/sN{6_M|V )uG{;s ζ /Zd.98݁gˢjoYZ^J'ΪwAfh[y_G>v2\뤨~AJ+cn4vhCo܄9J,]\[caCt}c[֏1dQ~B JvVG162BAQ7x- I5 Χq>a :~d^\r47 U o/!~ a(ˮxn$qЁSs%ܞ˱pֱYYAE@; FDf>ZE\X%Fe{14jΩ<$DU. sxF$M0Tgt/,uCH]-\5~I.ý-=vahMz3a03<Ҹ KulD  890K58dxXVNMowEԴԴdZΊdE.Es.RSKk벅M"BX#u=ba/[1sLj;Rn]9zOzr~rEgj|t# >㖽"=_5 c%:pzPW!+|m}|l.G&'O`"v蛄!,|?Tn4tEx&i9=P،Բud $Z@G2 ̺zCVXW )'Uͺ؜"L`zٌ \ WJ&A㣐fs@UJ|UJF|&?Ap$@eR©MԖz }73acFuS4Iu6:wrȳMlI|n@pR`?`o`6>NJ|;O鏿?i`-wYߔG1 xDDO5\2X|VAa1NȄS(oRΐɦ4@DIz,748EPqG_j[| r16'n}H;REp @^^HzSҕ+k@On50 ""7#P;G3o4;)h OLkMs!WaE#9WVJk|b]FY6!SHVzwί腟){m [՜=g6H4:b\p~t@p)9`.%RW:Fj!.,iItӸhY<ΫRKB7u^^&Th7-)֣m)Z5u-3"^PmX>yfhȧQ[H-y?TIskg0} L%ʝ٦C4 g ֹN9ay5Pi{-l56Rܔp{|SS̔gy2B2b 5iiVc3ri- 5 udR u;bV)ظ3`sР;$:ԼsolJoh9lXXh5{O8x8Yy3s(h8_pyDZOH"ϖj 4Y"ykeɂGȝ_h?݌?2cB7ʖDf-Xgql49w1jtfBD}G j.~ D>Ő"q}/MwPX W$ڝo#cF 8,۾ U}W(=[/͟gm{ x/lQni]^~QntK;9O L=t9G^Pz]2?PUL"./e02KdG޶Pfbw,e54PG|HBХ4EoGX~6kť/%uĉ!cD +'?Ā0eAl` ^I !פƿ4C@A?y*8z6I%y*]AJ4]WߣXN}NVxVᷝ}g F&_ҶTB*O'Ns7!߈Zҡw4Or;lt=)q'5[?xI'} y2wZo?j[&0)њ&=h!-J(94ē9+( OO r 474E3E:4ցTEf 4](Eqq`EA(Қ+q)0"f"0` L0 q+ΤpgBB &J.eXӅM:9V 0Ճ&α4w\g6L]"o7,Uf&2@Ѣ&vp֢jc'ҒìY+P4(1 .HְZ~tXr=rx(> __we֘rjs= q&_y!FN_' >]v/65Tt2g4S0:m=c<ǜn޷?T j0=AM-9ךwC5ժiW;p2k@{+bf3'=ibO!xwOx!M^Ad=\tɅG%D^ݚ53K{4*]h^Hw_6OwD2z}'S]M2KG"qN po-tTqW-?~nsD}꺛\H Cy:vOp'(80,O@cx |Js yߡyǿ<{džHs`RZÉͺ%pNjMʱv s$ص_'vdՔ!uP{AƂ{#pK iRRtGֿHͨk|UM&*$'";QO$zTnnZRa40:Yk@:ZO6d} #J :V vi/G< CN|j}cLtP[ }g2t=rVt\_6t,nnnm0|(RB1hK?Ttge !SS3'ϖ,G8͖tA\, G_]~r RA1 Я>{C=c'+}}9mj>|aU ~W+Ɏ^הLB<]F^vQVUzNxPp5x?+w!L~ sV~nł򙧇3 j5T@`JP%r۵'u?Kč|VGm x'[D쿏vBVAݖYG .*;<.,-FQWhgo4pS6 &|6 b2 YH}2u9q(}sS!UvT`!6L`$F6 R#c^'3πN0;FF18ɸČa!-VXj-F !`h_'Y ^00/KhʦASa@bR*{x OTĥm21=XMtPŎpGI ՙa"h2oWN\J)sޞuOw˪=XJbdxEGƘgᓌ9H `ק6j<|֮05Pgtq5Ǧ% zg3I:ҚEP6dY X"SN g.JhM>=NKnêO-QjVgM@+P*w9ө^Di{'!W詌dP,%/S /XDRa12ZP 6P6E~9§Rvg. WHc*;sAT~X4%dvEڼ+YG)wO;7'GsM!RtM8 w.)g\#$S_:Tְk~?SCjD@+|\!x9~uMv8HJƤy>:m~ M(1kzMZa/B:8qV|+"0m |vA6>h0:w5`B%Q( O\?z FEDb1@زb1;orBǐ ԫtxBpgJAqW+{a[t*ɖ1Hsy#}rBivς7BCCv*Y]%! B3:iUhiGxf" %K6v@/P(X0e4ǝ~a̮:oY=SRMS*joƴ7I] ~]sI57sjQq q :{}Oxh(.=H,\ܻuNɖIա_@@ZSoߩGQv!K CH0/5$& x D3י$G$8~W!)>Ua}&w mQ/į|6UT%)g]Ss@8sjC'Agsb! Z]vjAzV$$m4"$䏜DU:yD#3L{ĄX SJO#4]u"DgƜڭPGOA_h`cflo[O͌F EQZ  4${\dE_H"ձ//37t}mwB>TuR-&rZG Zb BNh?ԫ!nm{ u70Q{р zHƥ9P PR)OYۏ' _w܌oe*l~G?+5 P0z֦9!jŁ⃀+(2/Iki$DVQ@+` oJ$HQD Oy^VfO0 ضkM׍`596ͽ2pW)ME.BxKHueES/tǠ}ga~쥃On$$= hK _{DGIђjGo"-a}ehci&L(.?Lmn}ef-`8kR9HTBE[)I5z7ut(V{&!Ĥ%%YBjQҎ=Tzņgc5O+f )<,{7f0b d?qU`j`Nj `$,i,Ud@Bf~1VUCܫI[ l]Eb6LObP9s 5 w'͒r&М*ⶃ],,G5EŠ#a/ _ A( ^a30^J %ƖdMC&@TeBd#h7YFbcWr{wfA1/T$o0lY5uZ)K>y]x-Al՜[|) P'S6,s% Jjb"sxv"m(VV$b:4mKĺ_^ ,`~&Q6MWUh +)g:&r:>&,K1vZQj&(]Q՞v2I=PVޯ ?Fi i_t IfߦͼG#mwD^*R^\=$Wڄn4bWG~ $R13ilJz`&Qt(*ސZ4Y Uuq=6mlqL0}pS6Cys '`5$$DGoҰ+ǦR|>J|9PeG_Y˧ ”6[|m9%Q|t r-F(V}?7\.PK4$ƴqt?1)Ӧ{82F604 xKnC}4PWn,.ݨbYؒ:\v`>M;DEqM d +!& [c s6}b0b%~* VknpLjh_NfeNۯ#RNCXi Yo L{WÉ?אE|SN.lؐ؀7@~ia&0߼c&G9د?c2υBk/X喁 9ߔU QՅhu IC/Brm܄bI݌7s?/0rFIU`}!Hq)Lgc(tksd4Zj \Կ&gnXc;ݪ?mJxk' #.%COO '1(c6ȣ:-t(Z? ikਆh1EmhMWCvOuhXhd%)M|_eY] Y6,0 98C2j.}ڃ5yqHW(JYT)Qr>M)rsbp8(Uhcĕu-xzǝ cAfxL y~jg9f=kK8qtk{ *\JKU$eZc]?KhΥiȃY2Cy% a ώC44o$o p ?̀+`ЃXX[=`Su"$<A(#Zt|:)?0590NZ@.YDP-є9 b-?B#M׏(1->nU >˓R*|Ati䉬q ƾv Ƃx=gor4$L8-|WBT6Q-Kj2h&8yO/:Ox]M+x%vL׭КBIxOic;e.2&="cm4'o+oxDKo,I8DE^5"P0 MWfz1'y"O.F[XI|Vj,2dǮ..ה3j'ڳVGf~H6H=Pr[ړƈq&Mof.н]+.^Lq4\|8 AïU+6G,}}ιUuY1BH :1hޑY0"pU=z̡_$s6(Z2^mBN>1qFD:a2qa*N8651 8*14HG"MĜq[epNګDW.Z.4kGgU1D|KS#'WQ r0~Z:f^3za0n;a"]=vqCxmk9?921֬^ ;x ^ք;›3JRZE:bl]>xFpC?$k~h1*4o h +l/~M4whbdakC/kd_fM$d8XLV֒CpYSLxo2EBJ.fX/q R;#C DF|iÐOzM6|{mub8:S$<P/ϦIn+WfW;)}hJF9^^aEMU>)>C˷ Zp/UwaM5ZSS1E'l]* 5pڎ&f%쳱ٶ/6H-&5TX'iֽ*DE_u%pɉOd+=_#8YBtjƺQXV=N"M|>3ͤ:.Q1hqX2Z+hJq!B 8N|ГRv:f7y0(scR eV;&IV``͟pBftN' x5W,L-;x0sM@Π OghiS'+GaRLx>+dh-RQgTpaxT1WB -V)E3 ח_v|-[ b"|Gx-37k_V`ᗃ7Ë+"jbM>EQgk)ȫ]*  ɕ!TIKVwGF,gl/a=vF+ +Cl[0b jў;KN=c`T[?3eaLu?cF  pF&L+Y?\>6lcdHII! cR~A )̘|Z66ֶvc8O_fFf|ݦe)I3s;N_8f\nZ ͰW*U_uMb6kݝXh;<#cW/vWf.w<3TTmtk,hj`\MT{VJY /@Qŭb @S88!+unehww각Dh ՛Ŝ7QAp8ረ(`Dfeq.fq3W ߶ǐ9aÐ9ߚKEv]GzYW6itXq 7%H`Qxh.*6^,~Rc e-M'BʴJ2tMlFᔋX:c+"0x`CⷖL0AKlBX-o*3x9MWw ?Tc,}va-vv֣l?&&O9DyrnuOoJ1WLŭȣYjDY.Q=ʛ~H^da?˜K')$ QNEU_H4FK WK5,`C DF*c#!*{d4m4>r錅,>=S`-)XxM!#_Q=9Մ_-6ByRAOOIQّ {8k/Z=H"DKf6\'̠iDo7}YTب^Hz6=u5q ~Y"~yGڠr6&]$e# 6qQȿ\e#/uc{0xə\W(:6zfGM)w?o<a?a?`UpU^Bv2h#-WB)@{jcj7y6-J$<ۘjm%6;jHXO`'ϊ+ӌlkƱ{]jH ;2o5#Q_#(ѪEK^"j+jbH cvQ _h(H10b 2&!oq&bt'BFXrsESۣwұV:Pal VCf3itنfKhY%^AD͠()˅e*Y+LSgB!Y2@&\ৗu7kG |%ѭ}q]\wD{W/{yC-#MNC1~Ly9sgU(֦*TOsqS4jm))~OIZbtawNgkWKX+8Q&STdw{ l}ܖf1!Ʌ~ FZ #rR] :Vx@c!g :7UߞI$, [CO)n%/QvdA?@sjSpܨjY "I 1S.IQk7C[SW 1ZKʜ.i,}z/Jo 0>]^pʤ9?tŹ9D2 B`#*'lKJ=^"0x0٬cJ]'i(DjZ"`?1db ÊER) cvxwdoLzsIXW\?y93^m?tҺNo:Y3Gnjye+z7,ZT?C"Es <}sZ4=X=HZNOcΪ^=)t>ķi;%>2m% 7BYw~'6FCO<.1.X_WikUF ֫Ѵ柞(H<*>+}ʀ yƒziXYKne'\-#-2] PmIOtNL zOGW)pVE3[W:fCLqgqY{aXy'{nbAOůV+{V̬"9G g9DUO { oM4vmgqvs;k%V#GSwb q}QI$Uޕ/xpxݷ0بQy741L' jR*^c\hE=a]n-hл} )Fx?I*7ϭ V&ij'G'y5kܿ) sN-U /_QT>oܩ>k`[ŀ]KM}!Z~ɑZ8ܱSqSpDo2:E̼MX ֝ ڧ!S 8 Zdg);\J+ow3oFnl>[X_L-$ f8^s8ltfUGhRrf@rYy7-}}>j z/_HY$j `b Fa/F.Q>u j݋kj(}qe~f/BT7xa*JNޖǹ0,O6| ި#RM{DEX`|e],5vR#>~'- : ˦O:0f?7O2\Β~`GA!Pԋg, [,fow; >k9(hwG`j;Q_/%k>JES:C4/ ջp QiJ9r~i\=FCc -;i0$kEN))3K xj+Xܑju0N^[{Tzct彆6(|X)C&.bCMfwH]~:3)>ɕʊদʮK,2*K > ͲDP'*/<]hH<&7HaV5n;pÁ~&^XЄ9e@uTrUM2@Ċg"e|uQeoMYueeup& \]C/NXg]vF eOF=4?R;pѹ0D4}Z.T3G ~A! DGxmq)V11\r TBe>$^=))XrAׄ&h`5AV[b~  kp%ԧ "F6Dzmƕ2Ǐn+VGg_%>$+vC '0(2мK6Jj hԷp !#H ™psČ Bb&yLNO FcID~:NF^* -wVlMJv4[EV٢-3qڊ3;6]~EgO=mRk,vОezRR-<$jwO%=< -jMupW0*^V*M670۫^F=xTaLґl}H堥}qqDw.vTݺS!i<(4ԁ[ IkQń87 ¥c2Q#:%O|5 JiO lh7p\ԜPvUhP̯R96TOgŻ%ggquZ]p&zw:<0GhBTZCVZUHA1s(b#,Aou<ˑ:Mub8*}KGY5.$OY<D6{8;G碯Fۮ4m[ͅrl0@vN# ě 4A#hz 8Ix) Y#s  #q[PDbs@%#q>oF!׮q[A|#w⎪|a݌o8]faE;Y xJgA.[ij~ Kܛx9% wj !_%sjs 2kZCQ֯%/V53y)\]KTmR0RD<䑴bk@ٮnZpyLCloܛF$dBQv*]w)@ hI? [oxi2Kvܜ"axpKͯCI+Q -1 eVMICn=9Ϫ6bed@ծpxP#u2}^i) %Q5n3p!_d8"%Bu>|~=xB eeI?j> -icֲng< ZV/X0h&HIAaSEi'3f#9VՄDa.tLS's{LbQKkkHBO(5@UU, !SLѩNШ#lpʏSTW'+,L_2-fGS %V}p}􏾖rxB> އ#zogyzA땲F<*]f63P7~kiD|%y:|c#_{W|aҗK5~%G`2=Df]>2KaHu&v^}q.hG`j6 xos7g4蔨 4tK.{#@MT6Wx1;#1|GDbT5 nF[9ZDIĴ?J` ],A>a#+ŅD>ؚoeZY3*ݐb\^>f2`Anp+/`=B2)S5:U[ʶ1IYw>1 \94_6:w, 0:%cł,A:Gg׵Y`n-=ҍ5us'{ r6;\HQwv3UҚ# A:_(Bwg:|m1u,R3f{b'K\\Ĕ?*(I?AcM(^f' lZbvĪmƟ9051Ga)ƄE1Gf=6Uĸ3T\ٷÕ]Ez,w:8_=z!dMHjGZ6TIL}}Y(2h|`d3jh /&vp&8pmoJ,$)j2$VXTpT<6|ht#ڶsX we%o'&,{d~qUo+4RK8ty9vy>v?).E9>wNO7C=#.)DM2'>5'nN펄LGؿ}=m3|q'Zbmg}:s@P3}#7@nvP9]ڊ(s=9`ik+aua#`yVl;I_UKW1M5#aqs pf{'V05_+uO{3>w.8oA}mz>~TdKFP BB;SϽ;_90߾=-DE618>ϋf&-+?bd_b?X[[:ؚZgs0qt2ppcoY {bީ/FCDDeIfmjEBoτlJ^4Tܮ| zPZL&UUԴLYj?N<8ry$0W~.Q_.jcAWr&|?\{`.U&b"&!E]? ŪEDhFHL{ϔͷ^_O+N4 {>L+J6[rlGb*Ô HF4۩]|RUŝDU[?N4`b; AQ_P8u<:dѳ}ZE/ }i] Gb)6|0Zq61~tX1'a,~t {-j46l5N:N[ MZ4ҳH@ =FB.vks&0N KڳxY*M^, dxZ)z[KÐA1iU]lRe{^Ï\-Fl&O0Z(bx (Bw1 |aN;MQ`4L"z{}ռdki]+0E|zRY$I+{T) @־ϛ2`QKab[AӮSNL'~#OgGɼ)i!NۄyYt'FaaO.'݄F&R0%5O4VN]pKՔ, }-`6Em1B lN[I菐"- P1_(m+1BH!ĆjiN[H\\ލA[AI-㠉%?CXU+ށê/ׁ l =OZ͚=4sYWHmԱꠜ5@X:t$>rAɎ:"yqsb:EW ߳1h;vt>ĴZJ:\-HQu9!$4N{^vmëΡ{(tRE`iԦ#WHDvÌ]b.R+y_γZFQ'5Pe:Z""eo 8uݢȖI_Ɔ_]ԠvczD"Vٱto>6I#W{ݱQw (Kμ߁S\=j)+a"ZSkCFEB kb~fKGjwΛ'- LG(c۔E'4˼KB Dyl@jpd0,u͸Ҳߑj &WJf4CH)YAO vJck ?ҽOCڥBZID})orE z./߯sk:Z(0Bм>DhZo80_i4&ܓ8ž5dfN.h i0xDbsOÇs0%DXA1:0Ht85i#tHkW!3?/:RoT4I Wq Sa;CBIBIC=3զ;3/9/K-ȑa5mc.SmR?5GD@azNi!K}',B~)^lۊx!ܸ}_k#SsHvKpiϳIYI C C1 Asmki&<5{.m.$,f^5P#6 ] vmnܮ ]j=QRmuTT_jWv x-0F?a2[ƧP7'`-l'?/ҐZgk}]hG>Ҥ$xzq*Q<0FDE듹󞠂n՗[\@H;sbKn^oPHV?Ѳ.,\:vo/hʑөk{d@P{a^ q\w\[*hۦ8~{nn)TCWqǡR h $5 )K֓v5o!]wW&z}uy μAD*m†1q#H_'@Q#dㆩO_ÙJ A"g&<&,ѝ˒vJ_B'dMeP/taT93x4~ښaV#85[z/k(#Gt/R]Ƙr$DR0cm۶m6f۶m۶m۶m3yι~9or>?o!^Z|erJC0@+!}>tz\i(%ݝ奦[Ubbu>nJ̝Dф1DU(j!TT 2 )b1mJR 1U)l}k1~ T+$|~>=؍_Xar+Pk)ycR+JJkC%23 ϶k"ؤY\ȹ+33˕w".Zsr:;hZIN&c$ œH&@临j±pm Gb}8u F&@ՄLN̈́W)>8tn"P;IX×@+Z&>ڡoPXy Cߋ5|O*>獵BX=vXaIֵ@ϱ!A>P/ЉFXuV߾j6 FqV(zOe+xs'0@u80 ݫ  iᭆzRE}SP\.SݗPH#Lu2_ f>Qz|.~1AYg:;E fw y4ҚngLl$kt_lYB.-|(LU0Bi}Cd,d=WrQt*k2IL:(;4Z.mCⲺ&sT< ZݪUșS,NA)?MoVv볉 hD]=*+{J/U&hܱC%z{֬=V|9:{5ϕa_ M$}#ZC`zrڀrsQM=@a3Gry}岌Q( ̡g;-c,K{ph7aJdl=(ف\fF)!%K)J:!X;*5 g}=:ԭnv*:}ߑ8g|4mvFJ2ÜJ;SE;17bʖMVRsd:]O>0w<R٘rkmvxr`OM-S}pꗽȽ݆N3ns=pᖪ;~-T7}oS;n|"+ܻw-/Z'?3?GwX('w؁À7p"ф؂eIM&ׁ:}J&w)2 XҦKFw1 'I>CUqӢ3u<ҵaFo[گqdM7߁m~dƂyX[s}Uԙu kl1'>(YW!JTBX|a\3t3}]L"$lhN"9>}2s|^$A yA>)QK}࠺nUITޜm|4Q#3>{vx8yg(撫,cQQI+W[Sܜ1POȁEOMj KCs>q 5|eV+PqIYI;ûKkR|J9|eKS\'gIǨiGhLUUTc6}CY]VVO񃼁mNjY9*5hWYf|y23g!EC) oOO P#4OI G\6%=>D=Pm(t*Ycv:=rG?`9l? {x4y$(qBsjw;) zwǴ&ćF/K/m0>˩$?î #ë$~?(ܥ_= 0)CG"/q7.SP{ȀuAE=69*j[Ha"0]i@C?O;K|_O%FO5a\[DPl22-~M)@/cjK#v]p}Gܯ9_~N48Q}JBNl1wD-|WYT ɕJIPe2FCNJܬ2Jҝ/.k+KSVFAdH]Xf{@DI/P^wzvB%\KMNijrUE\)J1po`^n|,3 n%J G/}7sj]V܇CGZP2?a 0O5󲾸~wf n hg3޹HE}kVQ,GdfIq;tek&o|@L^sQV=}J N%Y W%MXxr4Bޘ$؇[B֏AaUdd?ɪPM,=v%*:|;?c㳷 W-5%#MԌs Keɞ@J!Ib\gegf\Uĺ(EqaD-0G,!xʔPd+'.42!%hIbV*< #X4'ά䝥зf4ubJ3h*DW&_plwtWHZzʆ(2mkD#s*[c_"j8V6mʬTiGv C'm)Dc?Yź,4J|Ts;BFUp+G @i=r kneW^ & |7.8vfmFwf߆ܜ8GkbKhKؐ r%bEr,Jn?'PQyg.T@k=P3*,WF$~-*|*)smX4g; < ((LC|B-ʟ)ɲatut^Θ/CI!Fb\$"w<$(KRY]ڹHqf{6stֺ/@1nLd2'cWq;SS S8TdQ[D2Q-܁wkz9*wN)>]boK)nFVt`p/sߊI;K``_evb Y>>nly0lTydS1Yͅ L WoyՅP n(cșfPZKUkgi)kV-t ~]gosண23,6ȂiLpYLLȭ=P`e&OWvXkFUTXSܡ%EŒSwi"mƗ-gyƂD\cbajUAw .6J+kJZotg׻5H@]9%Z mGB} j]k,KfS|ZpeHisi,Pks?}EPdt[l.3P@<$Kۄ3]<#TH_lE2tj".TTiG0DE[Q贃 0c!g$/ej;T9+JR'-Y \_'4:?2!baQ `  BKe<ĩƹߜsGnݓgK$*C|#p\u@+uPfśkU7਻sqzȖ'f 2}@D7dB$JuQ52{Vn3H۴]7q*SN\;}|G%x{zNqψ9./#L{|Oݠ^AxE2! tWU ZnؤZg <=p:ӷ|Vz3S^ϹaܾVZR>1W|KD(eaŹA fw)V ‹`,U?ը@DD;Ro6`=0-Xv}>ڜ5'*T3ph 29'&NJ4BtV`/'poUzjMg|g>cϗ ZiQ1cJj,i@@84eoLo.*[{R߲]P~~3'Lm_QۭPmV [Ic7:d%[Ank<HyP.`.~`IAG0/ 8# nrnͭ+ŤhSڲD 簇ڙ8HFcIDsE4iΤba-A\ ϟWP9mgPoLo~{mD=kgtJfJcen W;>' HT{j)a(W*ˌGg};P:[d @OYi9ڏa{z;6*zM@)_M :7$P\t ɀx,sF W]a[:@wy r7szh *jAcoLɮ[11~cX]dk/۵D"Mnȗqb]}9•8_8lDjkҊSR( q ---|+1:,nP!U,, 2?'R3vKhwG_׀k *'2miq{BaL=1L tyiĈʃjzذ8p%məLE?pEIK(Iq#`l,CrXP?4)&)s+iC2WʥK44Z9^ B˱q\8cSҜ=>ɋ`e@u G )Jԓ[]<_ 9ɝq7J$Y6c(rjZ(2˻M54 j'tF*ᇁO"p$<] ӛC"0B5Ƭ);Z^(kuJ!`fg??ⳛN&.7:8/ Ud j%- Jb*%l!;> {9pFcmV2?JJQ aaoÙ!F~,lֹS'R]c׏;RQ f ӏI,\jSǤU0Y4/6?3_lzN۪eJ֊)w!όƻ-. 2pn9y ZΓxi-JWY[)@A`HLZ䩖)xvNnౙ0VgBP[*$pnc+ٷL;Ǻ*Y qbC & )]yjRy*Rq HF*fNb^@E0bp&<-dk [7[yID*6b1 a0!eQwy 2r(KO%9](|.^Ś=S#ǴW[UGǯA[08Uגթ}[0g1]vM#Y]J M1\௢uU2|0'at:e.S*lu_ze\}n$4 !r@o;oRfR䨧em :y˶·]m}܃~_oÁ.͡tzn^_џu!ZYzLy1G_oV!3caxQx+#IcUEZinps_)+̳&WPQh5"]lPpU:?ccVǯi^WAՊ[oR}㾨s(R-}VYC5.Ԫ`_!k*r#]ȸbXzMf e(,ǃ]Z*2z[Z7(C ʋ"TwD';?lJ oR8F F]+){^`37ҟ`|\1+9~w%C^<~8ji$!}7tcP,W/-D y8rebf <)=:&lIIZ߾!WStaWŀ/uPkHvƼ3seC:Fyf#5țUb(Q 9@bg-moЪC!ИUC.M^n]d?HA)cS0<3~GL<50'zKxE S q3.qAE|]D0gz$'-ՠ .T433B1mER9GqhcehFNyQ/#B hdr3#쁀jzct&zOv{DBDWDND+ÜZCsM͠E / .fOmq4gNb-zCb&x7<]\AK/rzGPB>¤ NqB92IO/l)SFȬ?ABcBٌD)\u; O3 a&y17S,r_'&)ç tx謌K@|vxFĻC"I2Xjl0GKc1Ѥ⨫Lt-B!!!bd!ǟokZ{bkG7$rKܲ]@pOnU(Yg"(vDBWχݮݑLhްJ^RtWo8,lxfnWO|"Ҥz(Z?81WhYW85 %ڸzZZ3QvĈ piz|"XQkyCg(TSǦ<-Fضf0dMA+ V K/S֘h/Wa3sy߳c[L4OY, (*2#Ǥ;[8KalA?mE!zkT-OVN` c\ڠ-k^ T=usquȳؙTO:Zk꫆6_G5ۻz~7Lv(PmZ{{N&_Mdtw{^O>Ԫ{vv]^Ɠ, U^7jk}TN}ٺ:%GZ# yn'|AAT!IΩpr\;fzO Bw ϖi =M^nmLS3[9?~ffg= p0o*]8, .-\8q .:TH/*j,ho)Je ՂLNdx > H!KQZJQaBE7 P1)1`3KԔ/pKLj8nC3ÀG@B64q*\JPjK=SWL63x0"\@h`r2BBw8fXA6$6koWNȡ0B҇dH/T`"v:\=PeV<% `K g* )=M`$w3bL9zlCvdipN8Dxo* A-Fq7T*(DpQn\ -,2,&6҉qU01߽!LKʖū2~RJA<}R} !qOt-: Kf'2‘ف%؄v03;4Q53[@%:o?, ''qwwS"Ks H pw4y(֖LPV麸Nwe*R  qm˸ gj7NJV.4([]ɋf73h% j},Ș!LNTdq1>'"4fn(`is3 Q !tN ,GO$[u׀VG Rs71\\BWrleetNSIut.WlcS). Tş+&ߨKEa.(o v<(`a1,UYjk.+-Z*_1v"Uĸ =F7Z;gŴx(^GG hP>Jrr U"#ГY()\AYb"a=~R:#A5ey$DĜN !Zr҆ g4W "+( C0xA6ejq6{ԚM78ƳsPqAlhJI&bpraN)G+qJb&U]ind}gnFrJ "1) eq/sjmJ*u& XBG<5π|VbjumHm->r:kSpPoz􇱈ýF  o'Hj QZw-ؐ@ۣ w)W"ttrdS{)հ%hD<x']6ove9S'Ycx9˅W@Gát߿v\a4hTXm*KF85ϟ0w̐4 3΋-n^rJUz/Igvh#]Uh:1&MqZ($ 92*,Jځ=nNhߥoF%vٮK/{ RgҽEE3Ak/4%Bij=rVnhL١Ukѭ xI G>6TmGq:12-#GI]8IL^|*?zx& E$-S:qW:χRwO垀OK`%TB97XRm=Lh;?2'A˷>ImC>r9i(;>>ePM\x%<!H<)ۈxoD!V#x8mֺrxւ`xkJ`XZaJ,:tm U:‘5R܅MM٤4;F ~W7X^҂~>|Mƪ>zqøET5ٌqJ5(6`&Y_Yg"F 6a|KgYuKVA$iQİ1ZAyBeĺj1ˠ D[Rҧ0E$""G$o&B1$.ljPL0%}-o͌ #EF.V1w@Y*N- o4Du'@iލS)Rc趄@UsAͬF< k Y!wla?"2rzc:UhUUDu: U`ʇthQU 9_mRLJN{j:>,t?#XE%#C9UşT"*zBܷrgc4\, 9S]J5CnQ*N!~bgyUD$D"&qP:6˫cnT„-XBsj:Mޭtks#EׂQ0 %+S9MNǑ*UgJ6N1[lvD*s#*ygiMzt3zIţ eE֝ !Ptv}]jޘ `dr3#:cbuˣ=ό`jO}KgjFJ܆pέhj-Z[@ՒpOC0L`.Oư[ߞJa=vUf>h'Օ%fy<_%Èj-$kd1Wk%t$98A>xP8}(:reSW7ʖ w}Z&kPo^sXc Z{yx _\% Jj5]h+^XX#.+i6p3kQa 3N$X&!|T't#,{*:<.iC7aY5e&|҉e0Zp;}Qkk>ㅹsR#S[:EuRL:$ <8T y\B9G*Im4yk7Sl/hIg|("tJ<&'^t>g۬ԁZ*ݺI1bL׆O.x3pM"uh7ugV3swkgUYbWY|!G.!n,!;[14"j2.3V![kaXL`J^oJ ø~#^j"v x7HS-v (hE%nšo2E++hT!+N.؇Zh!L>d\6`PR>"~(RO=Pm>A?سWWpjZ78MqA/%-=Bẏ6t7&u݅ U`3T 4<YlZ%s|޻EO+bs5XCg6gO_"ZyH򱘓t֙T{'ijαOiN2gwu8pJ׌\IXF6qfFӶ~z@P2\ yBO {@}!?Q~Ѿ`Gw[ H]#73xP}"MwZ}{X6!є0}-qE5|m*M?ynЦpմ6CW^R 5:Z~m\i%rwW6jb[\WdrOaa{qYTY_nol]?HE0H !,{>hK+dMeMP[ <(3/0 gt3:|ڠ=G>J;tup>kyP۠۽z`;\nPi7 Dc7}P&HDfn&1V$om8h>Py0bOucwצ 0U_s:Hw3J׀C͝@n*lfNz6|m3 Uat`ƴfm8|zCmS"{qN&GG$|1vHՁJs^i׭G5,4)hY$nL8 D H -ChD"/39IĊnEcyģe. ZԺ$]%ׯ8eü[?P66]L` D9*΂+`Ea]KqsDyKb]0٠ SUt>PrT0(U} ҝ&,u׍z0T`ˆt /l 3]yD3yJ3OO(skJ>|mNk>tMk|Ok874u]wCհ. mg.]yol\r_ۊxOO t ❏ 0t@| OX[&}+wmP&}+wo_5&(f{ZA%(y4&M#q4a"SX4Cc'<ن@o #> 'sv4 1%T4MlF.rMd.г5f/V=gw/>{C# ȫP6qr)G5h=djQ6i%8S1D:M!_-{3uԍW(6ZO23oNV# &=Nl۶mv2m۶|m_ldb>ܪS_ˮս{%-Ɠ?kE?)O_,D(fNM6k54-Аð!!GpD2-Ⱥu@SUSDɌCw} ϲ7R>vHՓ#aJB'kkw̟*׼h;|ߗhh;[ ?-m|yHpvie]:bϲNejE|{qi@@^v6)6spJm4py 쥕J+="Q8rA?RFXZL* q3t9!<\3Oۦ|i𯹢(D 5ѪCt`5o~[Ht6M^. z+G8%oYEK<;"8-tVkک.VtAn57BDیL$g_:_`0@TjYK Q7*5M v'ā;$hlUhvjN=ϛŹAΛH=]{H M\#[騢V\vBk'ap1)ݿD'kAGj5i,̓Üp!Ydh\:2'LVX!ki ZiI_9w;bKRΞ#[Ժ4nXl7`5\6è ]ՁiZ}S]m1 7wiaVRah2wu "m}C 8@6ee~GO'S۱vb4ovᬳ +uw^.hloUTmfm{ !se[^[685* 2Bײ>kwlZ7NelᵺjPi=\2~RY9hOK 9b1x עrx$wi nO S#49&+&)R/ %(BIK](U2cΕu %Yn{H]=^D{Θ*eM[I&[-+/G}#pp׶pw4J&`  =tc:qiorwqa~mŨPdVm@Ķ> l'S@B7Ӄ4M.$.k#(.X#\iU\`;~ߜjB&4;`-ЄaNdzBLm/-ꇿ$d=~D RvX僳  逭MnmkNՉAZ TJz9.fք h.<,U؇C@AtBc0UHOxqZ?jj s];^@ 5ё+*?kw~Hm?=w?[ 2{[nzmFxmtte@w^ʆ.?#`"(YԄv묣 "((GpY䀔 MJyyFNBv{ TcxkډS?߬"a*KrMnmxM:fPf:1|'PmARGAIN V-Ȁ2lɜK|9HQݜ,U`fdng=;ɇ)焞{~OÍ3"+?5q/wOY T{}d,DEI'UWlwIZZm!62=j`T ͻo >`ίsW72HcFRuSkk^n/^z.*G* qjK}s۹$yf "ck[(vd+P.:7Ғ UW8] 0;s:ʶ&_alԦ`ƢПln:*9ͨbv)Nk|Y-lT4-ZSRw?dͿi`,Uj$ jfYԸpH41oAGШCϧBZRQ[u SVHO:sʋZN+}D<ļ~Ùb쐖q 0?OhW33"+V8Up0Xl8FfқTl$gt L-86@RB\T {˼FRp'u?nb--l[kPpRɎ%<4L)+jlFg߲< 9Dh[x_g]tUI5~lH}u~KCу2.k~tX^)Es0RW!cR1 Xqqﵥ80~X|u=5X[RM'.s~jmpýRV~cjɮK%vPaB~>HSY)ZDGm83@9΃1P&XZ[O)ZoqW9Nťd0{%kgo l!ϫ2~4?Ie Q(W]SJXwj@l}v/ h#I͕}tUZԩS@@CTk u4ӌD ~x7h^Nx=TI 9|nG R&la <Å{dfi{OuVE*\Z 010.տVJ۬uL<2h8mzKJo&0j2~CnhU}<=Lykg[e)hilQ#)h%=xf,X !ƻSJb4Ì)]mYe8 cA|Dj,ۙ|B&S:3b7!y :݄"q̪.$LqɈ&U^cɮf- ܄ߖQwʨ%#69m}$B ~p㩼s$77.k'=] ш;hHx6E6nm<">9rHuH`[)Vhm8ÛJU5z?D5 I! g䓜03sRu^ 4N+ׂ wu7>?ɳ$+}JSpYS^UBeYfïaȷ#H(ŇcҤ(r(pZ qkؖKL#њfœ4)ۣtz/ʃl#Kpxgو/PpkVRB5tUWSd+3uZr7[ OcZK ~g5JիcM ː\ڕvJM vvohFƎ/1&a܁(q@ie&G"rf~rI'[3DH11h&]$NEnȸ:y^T'>#j_N?-H6_F42~}a ^yK9M(G:w0uO[t.|;\wb Q7e\Ǟ{{:|(VbX&uy'Yu/[<1([OYi\5|]PӸ?|W] mN'_ly_ Ӽ;h?ۘYQl-&$='aQx)p1 b7DXdU { cE21F/ߙSll ҍs`ˡtCd'<cF)oqFocUƵ8-ֻjW(SL&sH'e״-y3+TySpLŝ%WX&_:s{rtH*i?z6n2 fl۵j5*Ƹ|n4Piq2̘ZI 'kF)I.ftLI NE!r ;ςK99Vܑk ?5SwFM\N~Lʼn`6d,ؐw-ڻB~)/h1=ӝqRд|\fDLНR?Ԕ#(6FI1w㘲d֤r3|+#O]Uu)y%e@GW++,ϡ*`N$ 3HD "pYdu̩7z GI_Ɠf}gQRK)'>rc?4.eB{UR< Kp6_aAfLr?ÿEB[f9) b4NYu%3lyVXpUt/D4Gzܳ`:#c/3U渊piLXbJYI(aKԱA8,;/ܦ0EubN.zOͷʊkqy>et; RM r4`~Brp'}FB_yw;.@J)e1KE"dEIlE*fzEn] _ǚW.TI>ysss./bz AD<\J w:\F[ɵƉLwvwS2Y,#JQ%@F P4jc4p6rH9ͽz})jtIb/Ge|luT@&Xw">;rtFXH,NVj׿Ԅ4 "o0i#Ȯ{BXvpRZĕpgPie#Ї@RGrFNoD3=C_0b"KP='J5kݬH pxK0yPHStP!̒ _I][֟r@& X>^.C{9#@% IxC)FACȀgG͈#N!-09lX%! HZLda_ Cȕ%oq"Tvs8:kyܹS£D)%&HƐqI""(`ѡ88,b~Z#zv+x9zp9? z̾> gNć)"`EP]f >,p5zCAa.Yķf/ ͓8n+ vtT8AM8|0\u0n)NG^쌢Sl_v3̻Aٷ`)٣Pа cToL61w!Mh`0aJl+ABң|LtL~2ލaԆro)N) {rom+mCxeNvm6ܽ_KNfM0BQd,eqeJRoA}%_GzVw!SPG*;| h\& 2W&5dnjIg)򖐠}6-ۓY+z4sgC<3L'ryՉWF=9 9ӶHbC7FAtI2 E}gtӋ3GN80\ɬOiX8Yƪ; g ]4՝N@M?ds3!s~Nc &@_\F:ĨV``hlAa|П:K+c7 fbx%zF#8T N9X1eAU  գ/veaŵ^PZMJƉ%(6u,W"xc*hߵRS,9{jYdž#(P#2x)ZڱKARy@*kES?P133: ;kdkA - \"TwoɌ2k0K̍KSbwߛЪ @i_Nݪ:MN! $MtNa0HEw0ʽ˦ԋgCMX3c-ېKyOaNɁ(WqXL0MŢ=.E1`)lY5LFaH"ٚ)@Q0۪7T Lj o9j@[ 'IpyKXQ:RSapM1HTzKA9FCQ̼`HWB[Lڒ%k񚅑ՙ7',"rCKSZvuKiȧ3o@=H5EͶ3NU`beIp0gkYJ6nHPRbP":#2(rԂ7lp@NT&pm2#uN;VWT r 5 4vf˲YDܪ5.95rQ\2:P*=ߚ,OX %2acs 7n,TP>r3~=jܤ$.RlwS{@ݕtQo.&qhӰS٧W:َS(4VhHz$7C1NMR,Wf뼯$J}#,h?|{:׷Qfs@7fz]:-0_l"U' i?;BBiYn5*&Ut5҆ <?oIoJNAivLb+5 R&8RKQQ όQZq|ȡG֌.IU@KsoK"Cm윤=)skZɈyWcpApLi/Ar3şZRwoŀhSLd۞Q; ] gq h-5p[z_!xKk|ma(Z,3P'7 P 0b+f5kY6EY*S:( nq|Ɍ&=a2kaB:DɄԷ KމDɒ30d '}1E` cɓOt Uy4&m8^(H3B'EKIjd{Ɖj9%B:*LyRK[.j|+ ;j:2).i }"OP1[܍9V=G[]/@0SҗWMǦ6wt K "P;u?N`w 6~ %ȑIjb-(Պu5gw`!Nh>xlwb좺%uk`m:5Y8٪+ )i⯑YB6yVg?LbCzRg/su֦*E_5$n!^UBy%Ey2+ hx`+3LsyG3 -FޭAއXO" e(2 ` ̵PXD}~k8h5x}œr4r fpDUy`,Յ2x$ʪ}ˤTKWw Ĝ`- 7 OD:A&|Wk۹ *`yp7vHMwOy {& Ӿh~؃+E}TjtY뻳@[^McA6b[䀞S fn${C<9ٕ Bs|6"f%ݒ]V*=cW49\iIijܝJ~Zm5;+#&Y/iw P~bA mT ad`=m/}B m:-걁ҲKpEM7R?'@[lJsS Ac c hւ ڇ@_MlMP"K*0]Br.4PC MB [ƭG +JS_)ÌO>BDLf$cjURQfG,˓!A. ftiL>E&aaH(G i;C#%`}FʓY#0 C]&$eD߃dS$j6|%/ՕXbXQf1&]`3rRXE6% Ҕ([8Ċ:BWD`t#?Ljx]w6IBi0in;Ut2QxwԡxlLVC0ѽ-ĪR w%G ((hVCf AUq|^G R:eOC-U:=zITJEVgcS OQŢܚkҖuBGwv xhDX(,oAt)< (;1t[%53n \Xk]9ֹ`͹ Je bxLױtU}zI<=g;}*-Ob"#hvf>Yg$Z^6SG#75c<`±/,V91ˤPiڭNƞr}}v'sL/\3h1~R26".,*cnmw,c"D2z !scqzG>鰥e3;G;~'%HWۙ#roKI!q`ssUeý2:>tKzgSpG;nt;2b{rQ'˰4DWŀd=ao8H \˩(GqjP ,AN%OCMۏQTԾ샣%x4ف\mHU#~upQ`d&^]wzɂ)<> HJJ7I@{>㧺RBnoo&R:~6ШwCϚj'tmw k|'0nzcF)a:sqbmj@$%Җ4X[rs4RBjB_.LK3~U[FZ.=m<$U29SE, Mx\4';1Dm[1amgb,VϾ+VE_v <7X68[iu$zJc|cj0|c+t]Qu>60bM&ΪQ[+`w#RJ毵6Mozn2{Nf%!Fy jIKЉZ拨Vɪ1ܖ`N7 Eɣ\+Ρ)}b0¶{PL|Ń!}jUD*GZFr"'mV[3E }5ō^8Q9k[ebsbK*R`ǥT.Ug)sk7oWƣʧ5 D%gH0e:|Y倎V?xml^̚Ĝatl!?hk,>\Fg}]YMva葱sSVK6$?Sh)kg)peOK{7QնoYiһdO\lҐ$D`2.ԋO']sXFŧތs޶m3A{FA@ DHi-=2U h &D_dZnm eМW~KX,C4,.Udprr8ܺ2a6X(^{"oQnGxaGy> <b?m 8m+j)3S㌛tغ8p D뛊Um]eJ3W(Vo?8E?:? Te*6BKr U(jbeYi2j#ܹI*6d[DrTRW%1}bj+ 1.wUgx|+[H^Crș]3 Z1M̢=R'LV:) tٻCgO9zr$Ȣ[d34T;/vاݟӘWVDXOzq'j֎1DuyұׅMV SyȘ Bw=])R$@yZ he=H6r0۲g`$'`ѽ-%5m<wNlz[[/n'{N;B+W [Z>':;>hg[w'y~)իYlZO%c,]h(:(jy=fÐS̊ ׂ`S"5Ek3rsY@Ug[qt'6 nхVc-.&33PvGOs}W?@{u/Tǵ/ 9)_ɺЅ s;POɔ2=CQ#"Kۘ1Բ qSQ}F<Ciј}jc=1jwª>-~N}lދH5 nPh|yuk:S K޹60oBn]Bz_g?VՑp˓"\zEnM\t;C/a?d:CAWWS`!_1YlMJC34oSHiN~w-GaR }fD߷1 >F3.3#EO5:D(ۢE|t[WgXAq9C4U5Zyù@SXltL,ڝU)o&?AF-9gvYމ&-^c<" =Vw 9 Jw1d ^N m-UfZ G8h ӘZu@E@\>J}J@ݺt_F=ߢTM, xFhe#Mԇ۾ @^1v[o\U͐}]_m|Lo{܏wn%0Px 5m ':6f((69U+zen1z O2tn. 7k`4B]lBÿGEwld8!Pey.7[*lXVJ.M 䫔dMVwqfiqsMwK:Rki+Wq?4ۥhӤ~j fm47 LxϷ}+閫ez\2]rvR#1{|+cai*W5a 7+O,ˣWXg㻒hhɻ5"'LjJ~^FQYw>ㄡ|=6C lZEOU>Y~U v:` (&g CN5p3 6GWP:Pa zZE/o4=8o{8vVrD3/Sf U>mvT TE"r̿O߻YsT?2Ţ^FxLki3C72z%!S)cW: :"oOӗAL[D{e`m`H&:R]^.)d.by Oм&ŧʿ@yMgCi򥙒vLFK]f@i:D6F},kP[MJR&%xmyҒ`Y+ebRA!y%dIXP3"Yۭ7'Fj񋤈uY(x`S aU#rM%dK|,Jv?IDžuR+Q׃;ĥu?! I/{:6oAg#ʹը~G^ҷ)nԸiJK0*xH alrS1H'm_F0SaG2c~`AlbФwIQҒKi1i!(4F\Ί} ]XGĴ$^W33.XzG"~9CV+gR4d՞@7 x)S PԬ*/vFRmɻRKv-c [:;NtNkVe=`}NXS^y>a-㼲p34O7uϐ:j!t|4J>]JUr?ގ$@gLƞ. A Ќ\[*=QJjEܮ,ive5Z)N8* w'@hr(U\QKYSKg{ȶԐ`dΝ'::? ͆r0Br7Y6nfRBm ==tb)j.zhehtzRNI=xhJ L&-U]ڙqy.cJގŊLyFa`g:t%LMK.CN\4Ej:.` ٱnlGR†-]k8~"m+B}Gҟ0 \eksm"˛r穞8j(*W6TU55J25e yko싧5^ZmE:CCVmCG*9+9"M=úW=vRGcdgESTG:E$BMzfڥfx1RY_+;V*ꃩ88`y[&-&لvi yc#(mx~r%tc  GW~=_G [V|Hn>_ 9%״ tJMt^)?a9uYM]T*4"6]^,iN Хl3 X7  )^lOdkV>Ēů}Dw1ƿ wg tP|Y#t^Zȗ -ld)d[)SάdЎ$HZό%4L҇J&p``pjCҒ$9WJ>'*4SGhݜd~ qtMS+kMT + *-2%<9Z HΘ.jGU"xv[ kwrXǫ jj3g{IU'dٔdB'B:L]YھU+/"I[vzPKߓg^/K&%+^dl]¼ FvYdh80O#V1-Ө,dۈy~6B/T Y[GQ[; %hBmIأMLNLI"~ ּ (,H;qX$벚髫o8bէl^#As^+5&TDMݣ[UTs[pAi!ɛF6W0SBO^$˽gﭦtH*b72a&v06lp=@괎"N1x|희;G;=]}ZMd!NeA~x=Kj, XI3JoN%vK^oƲ6z 凰i0Z>[!GyripJy3\'bjj&kGKGGmn!|\1Y@G321Y܂; QMp[b@Q6zrmB=߭Ԗjpkا! &ctv֯]i!װjfH6hzq% I@W@R5RrX}d+rʊ$\e0wn?).Į=^i K^kpB'V4SўV ^\sB[3fž1oA4^:w0˭aFw]?lN膃\,CNs{{2`Y @0\7,\\ %߃BOuهC /'hjD . $`]W_`ޠr YCPٌ;2\VrutR}/>fY‹{ZfOTQlBnj?UtKUd$tV籏F\ uPx5K F^N$ 3޽vZ7֌.M[갭L'6GN,?=gO<3Bߚ= Z\!8+GOdݍf]y^A-^mX 5s*`8;`H;2e%* Z3[miJVӻD2+aug6q* bWWN獾kJ{|5 WrӦ>kk ߭#1R4&T"e%D"*|C2@VD?T Nj1aQ؀COavzVٺz :;˵Α1?@}hOG/NjhZ`Q:29eLȋWwdZaV}feߘYlHjɗf5|٠Z=2< \=Xݟz_m~Y{U:g5d_QC~݀_O}%8`/@YYRp/ChJ]n䰡K'Mx] ~vxؼAzܱD>‷]2$A! [P=l뢰yA$6AV0]xߞ=,M{GO&=GngvY'J7C] !|c |]9Ch䧡w縺gԾ~<C=O)@#tz-$S}$3,Y3S|a4rr(5Ja*FivqI':ZuD+J>wouoD0m8h;M< u=YGS{$9_gfc6nւf.3h'OgaڑX!p;65-bc$Tᔛ1&\l7 ME#>R폿 =# >a߲|! F{^#AO :C~sI8bXS~"(D$oLs}z>}r*_ ,沯v,m V쒺N$x`}Gz44O~]RЁpռ=KSمbPX/o"Ff I74 x3CטnN5~iXW/J؊*6:,cAy%ju`aE2"1T.u I:H-4otIu Ufu;%0]f$/z9~TN^ f8/UbN҇Z(f0/U2,jg ~d^ .o{x@OnB5r{{[JѲ=:?!X}X1#nB`4 ®27utN 41@1-1M´na8LҚi.$bcI {B^̭;Oĉc ر_D\sjv{ ZCĜr/ZL[:"rLgUgtk+ña?E~o#Aov575O^0I"OY}=`A/C R# 3hf*.Et2xTx`:^#sSd9s e!N;a,h.a[z2$bL%ĭ8llhe!}_PG빱u߂=4YNA@! PQp(&Bm4KMӛC"8TNKG/%]Q-v Hg7>3vxU>b;]!=;?.)k; vSwAPThTSڶ~VcjDqnkL^j{p=>ɷM|>4jygseb=;%tomVKHa fʎp5EQ?~!<2N42P4źyrwm'k!C4vˮۥTuB (/ڎh`#"ԅӨӝɤm0>PnVhtdQ- hK^t}8l@oAސzxMMQYYCY7Փf|Nۈ&"p^ax7'r>OLp+g7@;#D_X[S#o'ś*cQ3ȣ  20RCXc)4|0 1?DhO ÿ ]@uc5 qپY^ڏpgwuhb1ۘ֬Qi KA0zL3TنwRڞ'0Y=hOIj|7naH1[|(h-SF?֡B&aX@0W0~B }LwqLOeonHf 90%-NAUhk߼v$/2$~"H}| qFrv526I>mD"`$w81_6~s`/;t-$E5yB􍌺xRV='_6:Th*q)AvvRzz_kJ܎Opwu$`(@(>cW҅6.75!uЏrWrƧIFDGG`{FuM 5V9`q2q{iwZR7vYMn³XC棒S1㰨cìY~_g%[ۓ/'a-6ёftYB()M.IA:?Bz0}#QoQmDÞ=mT\Ԁ{](!ܬ4HVP(M0IQoo azwpp|'Ûh)XLn"Z.\ CsG@' wbg(7+X,*F5wf_ÆE{/8ěU^"uLs+?c!-Θ{Zlw|djUopg9YwAkx, sQ" ^i[A=o`#줺Q&;J́CmEM'?7f:hFuiNRv=h-0mzEe&+t^nȾpCH3۶C\[ש ZwX{CuhqD_Xš˺ U/BH߸׼ F=WxԒr6b?I}cP!?hO/ /tdzIOdCk`vFk-:Ojhl\O_S>xn;f3L;6Fb@TZش%AdD Y$coR9HّN02u1q0\$3al#b?mtW~<ah12;n18moD7]`1&%D oNR՚s4_fcx/t; eCƶB6`?sI5qb9!';Wah\h,vQ Y60Qo,-Kqm>x!u/ܹq4Mh©6`^g`Y?c[Pח5J":C,~P"~lF,{U/AFuOU/̽quGEs^ϰury_<7ϱCB~UC/3hϹ?${U8o.~; 3'O ;+pb8ϊ}F>#$Xi@[oh Jɺd(- m̈"&2Z0le馌:?}8=`L@}inRk ˗B ޤ.F@p8-Du 3.Dp4٥iG`d2k&f)srC=[;GqӖE.ω|0ۧ 0}yڌ2U~ҊVJG@XQΓ48iw}ٱŜӖC͊/z8VUڿ_$3[їX-S软%nݨbS@ ԄFؓJ3vGJ빁޼OAnY.kֲO;QHrFqX;Ld>Z;^햔r<)Tn,AOjAaIFi%hVvPGA5| {u5E|^6uG9!Bh2 >.im7 mer8A ўEMɁހcd8Y9ۋQJ׶@E+$a~mov?NiftJ;9Ygֽڤ}h#+Y~5;iBFcg)ˡ[H~ޮ&A|Ǯ?|&]F.^pAv:>ibO8hnIaz jH(5-\Zq٠D*;VXCBɍ\OI%TKo#HǡBqR- iqbu$Ͷi K\!hy<,fE/ܧ4w&-a)dje'p Ѻ%Nj WƓXzQ87}8m9Z3Z3 >G|Z3KK?Y&d'ZD%~XTQW.1J/+yO)bo~2}(cձȷ'nBo<ƾX8(i^թuh ;5#4 pS Q+ʳ{n~b 2(Ҿڮ:AC9OEtDqۇBxcj;0lg٘:%xjt? H0`qi-ADfEw8ʖks"Ň 5K6G|5d GcFiD ͗,<`ZcE- M<6^uص.G.rN"8@/"9@dtA`(M_#@bkE@L-bbAH[?ߐqÙ(dynƙˆAT7rk]ݰ`lt:uw(I~0|RNs&3 yZ|퓆c,!g!LkoQyܨՊffSkە͉VgDž&CK$}ဎnuB1 od)}w XC]EhDY|eXVѠVP&f\&sU0?T`Zֻ;ܙEܗ!}]djF΀FH;xTkeㆻ# />b&ۼטe 4άZ^L8Z~bљ C¨ٳ} WO;x-W}m뉟TrK#Z } T+' d)H_ƿnj(A<$x\慄Ur~X K _~$4o$W'FJNͰ8'N^X&o٢7=XJ<&DYifzM˗GF?'7ݾ dJa4:ȽB%nGC+b(cR$N;h4Q9gͯTXE‹●ofn ar\CTu|O";ܕd`f'Aom&MPyh3r>_}cOE,CèvϑDT~,vϩ5}MM@wdM_M}6"Mmoi k,gj>iR6=+$t+h3BVš.W;OVoI7qFV~27^TK[)䍶voiBdUH 02PlF._=ꔹi.oHMvз%~%mkKvkË;5Q䃆RJ!+7N9颫5ҏĮq;[kf#Hڍ*V\J]ozl:JϾ!},T-9(ۨDLZo,ՙ^^a7>^ի/^?J&E  !l!ʽH Ao7Ŵf-AԐyMz)gM:5n NZX""4d9fb{Zy z% sa?#y!{h۠ito_yB%hz.0O dZSqߊW5OޡeĬdZi:\K&*(Һ`u4`F`vyfm*xNG>{}|2˨rȏPruwa 4kecϋ_/8&~R ]\.r˷u*ࡖ=Կ( ֭WF}.Cc[yo!mn dj&߫5X J ̊CskҠ[KӭR s㷊<~a 3ی6Yq {Ed-!޸M.=2\_adY*>*fz7՛q\T<&hQ{T pכ_Uڴ\[*5N? WL$SE\l@im v&7<ӚԔ\.08@ 넮<wc]ahŵ;m18)mnj8tmR> 4ımɉ2 ki\j+y #⒎H[Uq M|BT$hc(0`b4fbX{fi ,CcoKͿl,)VY-c|ȟ$ v:lNMgY>=R?/{`)<&Nӎ. OD2L2T5Lƈ*&2w[dЯdƘsХU ;֯*+.N=RHUC:!MUu,8N n8)&Һ"JM;2*o0u r8ĨKi)Ct%8@ls,veWc 9h$8s֨F3 \r~Ќ=-wUZ|z#Ye!JPC\7EZ? ڜtd1NȔxbS~ 8?DJݳ}f84@z e)!J*Uʲ_/lŎ29"-Ed]/3Oʜ\eW`M1'N s+6WGUeր61Pr܏n X~mz8Wq ɕ-d'II< 3Bbnԗs*5ǃa4W1$EZ{V%ע)ʒj͸xM9c(Fަb0Ph(od#w6W?`j[H匤T`-*mR*YwzpI*q=~IGד=k2dHOJfrVh(Ղ+f4bsyl};a1I(ᓶ̙j"~p鰕y>jeo}1oOpLbikşf {=V9 vxjn!)>A¸;,Tu=0.aϙTIVu2j'Np$u_nh%33:"F؋m\P*LB.iP⻂P k!k\c{7FX]cTI" % 4Td&sj;P!2wV}ո"wشxѮ`nVŽR9z޸Q vb p4j~m1BeRx4qoYׯL.[rM:L8]ރfFX8_l[%K@`fFK]Cd^ 躁屨'ţ),Nrל;k˛\Y䶃^ ü7,˸lmfwm?OqsE¬JU>{AyU--e }oH>58]tϷ ]4'*G V#rx`+jŔ~-xx"eE5MVR[7:bť!r3*| Icq|BSe0bY`yl%-'_F`kYNd'LjQr3˺{`ӷAu(*Bq"fqb8)A\<0gƒ/= ;p8[VA!,āR ddϓ>a)־pwB]+b Ը!pĶ!o4&ArY"+0 46fԯN i?0faRC &ָdo5_3peg>p7Èl"[U"6,* L%ti8FBQI`T3>9҂1Z@AM@5P(Ž 5>$*<᧌Z1LcVF`9a_ >1|4s)_ڷ5[l1i$䂼Tk՗ &zYu,kqmkw".]dVeoo>v 3 \I~qmzlN(Ccjѝ2^0)ϐH$jQ_TŲ9l |s|ߨ|쒤 3휙`[Qm&Z*H.2}uZRp]QlWD3fmi2e0 :>5*m*Ve/[ RQ:Tx,O>|EQD$_I}¤:F!9G0_Y17+'dMUj_a>$lK?ZYρzVͰ7MDBi8``즧D/b+]׸\{|`ώ8Ay?5$=xY~._O?AG7:HaU8}o,M7Yv2" ӸE7`| CnN;Ƴ,ҮVe}U, |UV=EE5E5J, @*W_0#fzD'oXFRqf!#B&Jq?+caEUɏQ\D? 8=&ǁqx)SVIH[vJ 3T뎛 el+4qWhMq-* un/]p`$@c>i)l8~H.:*aIc8vTI qJ [E9|_Z@4;تv]t%ųK #?nqn<}-3Mη | ۇBU582`7aj?U#3(NS p<Q_tpH>,CE1FNOn/À'j_ 9|ՍEj F|fM12$i{ u~7͘z0f܅4JTc }GxND @)e1 I%I{dp'|NwjdMt;xCE-NAx;T4́# uej@*L@_SjU: Y*jctjtfcM*EHye"1a! 1nyX:xvyLܘUd&mqEQP0jp 40 X׏US[ C !CۥL|"F v+X{"H_g5~g2yU?y2r`QZeXCۅ]:q8C (Lwmƞ 1Lչ<[LK]bAdDvE*XʄJ2DS֙\\8RnLc)u{RN(k /p9iJ0U>#EՌ~3|OY)0>}^~ָYӞNOyJUa1㒠[QtNKJ{ef|CQSkH<ΪZ=h|TO7:@!r,`m﹔#]}n-6v#Pz$H{Wq;#BTSggϘ\E\llȜ;3\͹ƥ<`B%mCU5jVcto<~jc[NPf4헕?nOoܾUOw6nzNzXv0d?@\s?!O"< SuF;>F 1HǠo [E4=#Fՙ?c?F_ !2$W_!5.{?d\»'i٩z%q=nȟN\=(jGϾ!틘p d {lN>IߗP^xd-;>Nr{2g!ng;u[gE rYj4gUS35e tke۶m۶m۶mm˶ߧ$}cfSz6vPY8D{,>beE)/uHI}^O1~!{O;{`Jз#z)ewÜ5H7NMݒaVDeL2)ku !#y$>P򩖲tlF(y JtBee5>ru#O9vB܇7;g242XLxi&8&]}0rm}Lq|SwNx|&$;XQHt$?,iF}Q8}gmSyD9#dQ-xu25,_`w~'!2sH 4wy /E%Yy*BL^܁Û+AF?J$Њ]PQ܃ ٞ; .JOw&t6| f _I5W7ggUo5O_LVRhG _K_<[9^=8s6?3w.&ÅLpԟ0u?AaEa@a$#X`v\G(0UfjxÜ1e<(gG00n$ˑ7XOoΈwO1d+adc P? %|6^{36t`Vkל֟ jB ^0YxjmOg)k˔&KMlI)#8̓Y6eUq9qu3lVKR\Mjaqy_>cLdyO/ubg f~?FЃ2ل_1!Oنu~l/jDk/"P w{0C p d,N H_^<~(xz7I!Ų&p!F>'x-Ԁ2F9kI:F=-z]bH{yMUNE+,u7~{$WYZsκʾ>kǞ'>ocOL\YmJ8ma7onώޱ0 ܊3Qe;.P=1 M{ܦKMWWDUF׀U#ZE[tσЭ5ZL*E& fJU bU*MN _TSȨ11Q9s*N]2kǨ=dҫMkIrԸdTA%T&Ljy"骳5ۀrm @٪hkvS =uUmMruKɭ hntkLԭ9hxk^*\nyK4B\5*4*Yʭc[Jeumۗ[A{bܤ!DcK/0nʚ-[sޔ&wk\زE+/Yj\p+G#M<7#O@mCHӪ{wN)Be_ʜƊW>Io70qEwHPwJ*EӶYʸc kb kr+ՀB) `bu@xVx=tWtǔsX.;ˢ 5-q&V ʹ!uQu4oz 4 WZ=Ýn!u?LN̘ֈ$Ͻ'f&v֜3Ze+5O G=ߨVDri,,$gH4Wkg(͆b[(7gXY㳪>uzCoV-]jac0 !40* RzBݧkCT݇h ӵ*+ԒNl WCei'7^1j8$bYd%Z7 WO{Iʸ&/s>qշj ?'UZAx^j/U7 $xx9I0[V%]79ֶ2Ӌsжj"1<vce̽BK#f"}v 7AHas -!ڗ =aRTt4v*t9 :7өRғϘ {đTf{[Q SSЭzȻ@5m@4H@T9 8 #+FHP@\S.1/jSCCM!l'!8X 'rIc@i@e \HifH^L0 VuƢLp;u0"`Q8ť0Luiɏ+5Njw&D{SM$ށjEaa]?}쏦:y?!ց$@_"$#0;pIB!USP^˅PlCSvOI4rztnq@ݧ)v*EQ(E%)3dW^ `.jJx7j>04Vy ,'3A )#?MjهHR<ӧig-9AOor@J@"oMvܠ<СGuNrǯ?|q&͗.4Eڣ 0ezڠSa=IK)ڳ+QNNKt8:ǩƿv M;2U:v,nW0>O6;sN8Ə+ _YLOdНeJ_3A! X$&<|)i}I* l$sMx Qdʌ9FY-+~sɋuːq&XҦڗDeۢǾHmk{ԨOpR$ﶞzBWŁw:/Wfۍ\1l5dt+kKDE @:1#<4HM*P>)أbn62bH!1nQh8bNN@s$Oz C! .;S8 )|ѕA3&67 ۺȔɜw2Ip=3)NWo= w8jZa =/%K8Ix<o"%OO$- ݒbHRH6{_t'vis;ݰ"c>]6lu$._*o<|v\\q2fǬUnΌ~.(4o aCs}x{kԓYc)oXl~J穪oZ4{* bMG۪MlߙRjZ%^=CG>ykN>ӆ2]Uf: N!{ lC :仩7׾oEtLndۈm*Giגo[ N[GAc8*ŶbiF?ɨm1UCȣM %{PwГdd$XFgڭ{bwg %M7jJih&= {^o[|{mシ#K ܤYtH jM6k5vde\F톭 wCH <+ yaZ]pl/h@TAR9ni7=/@|йeT迥XmYs^P]͛賖^ {`XpP]kiWHcLL*Vkɝ F0otn<߰oVKkn#le֛Cdu+5(yiK_ #{.&؊ r(3` 0$'-Сé]³T+ r(ex쭨폟[Mi_X٦Jak^t;cq5aA!*OxRP/': "(y7)򊼸tS(7WQVR6Ț3*+YC,mKoW?2eq XKMVPa }ܨXڴNDZ(t|HîcW*]08y #(04*P$]mۍH_1ykpPQ"/!c<Ъ= Lb#v~K(ќfOu LXNIۛ`Pevj\zՐ:E*CVL`"Ƈe-J9` ( FgffnS)n2Hl0*!} Gp04j#!*)ܽNgL1o \/IJZtitsE㩙LE(9,Rb^S]¢w|QRHfIlhhr&\`,:g˓8vSצO8%qV$p\[':&y e)/Hk m>.o"d۠ ~!NЭ2J l|uJ`)k0cAoQQvC-$aQa)/=\b  &G-5T'mDn:OSʻیJ|,] :"~1@0;ZVW7OLXt6%%"̆NL'n_`aOሯaH.=s0tQ3慻ni`Ng6`t2ttSc`} 3qo ;#MKQKOGˇ| vD5[XP FQo@4O'=^i `["oa?jH1_07{{< 0Z29ORc7" 8W0 5b#]]?Wb.Ix$Dwbi*a5YNïC裸ۿ1k yn/k y~L LЖ ke$'LYSO?PO+֐Yi{C_뽀4*XPcq_X<0Oi73AH R//@ qȘB!jbCB$e"'Zh.Z+Mh`t+anmdj57k mja|X&[bLwv|nOn3ޙ)й$܂ZcwyTv!5 ޵Cl,/ՊAƮ+&N` cj}eYT[n1-߲dQꮵzdWzHC;K uiznfcNԚՙLNXFMDG0bi_ZRR}&ԔLy(̘;O$]5}㫦6wÚ!\OTqᢏV̜Ì;Wo]k AA&9wGe?[ʵcCrއB%6d>XgD>_i*T MUEeN"^X| @[ T'+qBp 76+);NmÁˣ{<Ѿ'/Y`h@T/6:% \G0L' h!C~ƾ될@w7LYB^No-n3HXsb(`+sOqh9V[X?C +˫9iEBO:bp*U[۳EX dPN[@slāC=w>*m*4@޻ZRQqN dCPkVs+=Z6~7\d jw2Çf@,{KSD%,jjfzyDuelS^!ՙrR=Gu_Ț. i/XYF6z|NQ /jTm;ުFsp>zl{V2~s(*KCF gV9Xу׮ ţ:?J'J!\ K̳;qa :j*2MFXJ X\tEИTMzEETd*fb_khFYJ< n J}j]?A?saKԫ2-{BT2)d.0 me66 ` YtCˋN JG5T15+>oS 403]R߅/Nm{aITf0FZ_[F_/rCwU5XE£zޕ`e"1y`k[ Փb̧ʒm\䨶aa=/ 4ܸ[2|YB-sԳv[kcC̔mbA4t zv(|nUB5k[Na,7l"rLțĤU(['IϘ otr*L ukZS5& ք%cصk-6b*AԺA+׹b/Tr =ox)gFze|2~'!9OR b7⩪9-D][Kėօ7ҍHK.!:O3*<8F劲:@M!⦜@ѹ|W|&@IQe۸xзp;Ĭ.%:$rܕ@J>Dw;͎-~ !zd(Y[ͻz̶wTX{B:A#Io@VK@9rW"mO[0~!;wkhs/s5r­re6B(8?V|`)3w WyZ MF=Ğ^k`_|[y@kICA^M~ᅱo顸>;;"׼;t쇞Oc_YO^ >e7+{޽bv"0c[:;`&b=('rJ#[J!WuJ0l'Hż`#֚ @q,LSÂJ#&J0>b3QM[9i*yKԴ!/{DM=mϿ[/[F=l4n|T6uz>NEэʥҢF[=,KKQ gu7IZ8 qs%d_'YbF ȸBt4MyTNfe񑌘 g.X'CyB崖:a|QR/cH~ P/ =wC1IWZ]vgri -d ({ՇcpdE >[2S,wƯ(_l];p,_zEf-û{ĩ 1t40΅eYs$!ND %x}svL O'Ŷ "383+P:%e`uVx{~}+kÅ aډKSVv:qkJWPOF-oΫuWپ/kܰNذ n~D/n,{H%;ͯAl}`@f+}(\͹nL9P^u2im}Ǐs *# 0ܑBq4Yu_vIF=EprŚs{לTW8Ttxd&<6_88[;Uv'-U ih7JP-<xRYUm ZAӽA/U4A a0lN|'MZ$ Td3^Ό|a Q"+OⶪíWPPH;(N:.LD-#9_d_ćQIyg͞%:O8I_[puC_THw-dT|0J;L-ER#o}P{RzxlD>'zU)., r"cIxk=pFvDvH\~ハ{_?49kŁ sr"&1|d;:91;dE\$oDv%TC8Cک..wz2Z8x /py]nwC}~j?g }s+[KdɈ8bHd+Y !m?tZKyO5hW{$yo]R21nc l rjl731 >򊉹6!.WqyaʖxxTΝ )K<+WErA `[^}՜Qeq؜)tU{=ۼ@G,on䮂 ER5eWH `Hii k@ch{I56 g)陁p]RTAڙm;:Kg:D/pO02QVz =ın73RY}`}tp$mN*8(w'F`X/⣞KǞQH.dwBd>n`kß8wf:3M9ŧ- Y?%O\YaCeBR>~lCY r$Yժ@'|T9kloEPʥ巨dhdkM1d r6jW!~pZX!C`H)\eaYI/sY69A4ZɊy1u7M,K.7,4ڷcBre>YVi>Z{5]zEm=DMIb-C Yn$EMlJpC6@P ”,!fbVj,"0-~FJYXMXW̆M8.0!lp9]gvHY`fK 2'm'=لVЍ{Zu,/<%nlQB(Rbί˚0\>,N%}C* O˜{iu|&k$K:[r]V |n~x3nw}I0,^1$'oVbT5P2b}*?:up iH;eiLkM8Md~j!\:Sט޶ (y>%3"KVe_YGr͖` 0'e8a^y!N-b-P3U;u 44 {z{e_@vY;HГ-qdw>+k̳RAkE˃K\eg l[%%0e;5X6~NwdL2l:3˸zD}t*?SY\"Zr:ZCR]RP_d {{ɝM*jhY\Vu9M""Z&z.QFsޘޥѲwȻI1W!1pKJ_:|H-c[0:\Ѣ#N\C=Xl;`,^FJDAQQ^^U`Xa$Qfg̯XIDZ ?E0)/ĩ[yO;hZ3L ):}jMتJ*R~ي1*EjIC7F}ࡹĠ#YPrnr.,a)p`eBߢ KNppy6ka wQ |*^P[s"&-S$\atME4+QO;Jˁ|TCk֭yР&O]QK{`4ja"^ /b{ 7d wo%IgM9ML=[Μ'ݞ#xҫ@9? ud}cl1)_V:ߕP:̽_ȑEo0O F{XpqeIy-*ɂ㜱9S^}pJb($TZ(ӀA8I4E"ͪ 긣H3{D%jN$=`*߱R0N&@.IQ_M4ǚ=?a`?;<hq=Ävx@>OhCqU CÅ|ZzBqRn "WQr=F ۦl۶.um6l۶m۶mvag2a7Vr~Z۹'GLIC pr~N #14pX(47@FS iH@QCc18C?J=A$r,NQV7ؓUDQΘy|_vio>pac1J<}Hjn5v}Ljh̭J_,r -D"2o5e1^4dV4s]HNԥY j8*P4OIQiS2it,be`yh621 NR&/ nc3"6VhjkuKM܊Z.ڡB~M`F4gW(xG,/i/OVFKxA }^Du%SVZgG;y`Nz*2SbF^9KeyGAru3)+aΘZe?T=:56Z{u$V&aїG *ÓTVA^Ū:V=h!3VԢ^c WHN^_OE-,RTTK`Q MLqIbM4$јr5|MP}eCN7XxFclusJ5l6 c07#iLt:%~G;n-ĭbB~ma\[0rs v 2\Nyge쇇IXb3Rz&U:>P|0ɔ"O~ !fU u0wg,5( i]SseWAdp ;?v! r` Ś7H@@| @ .ޘW -oy-ziK;>!WQH M-g@/I5?'q`Naw=^Jĭ=eA#0.baHm8?,P!noQu>6Io}`ߕhGqDqA>cӊj0xMWMf[}nŴoP'[Ҳx3FTp&6S{G{`Va@!t)-׍I LTQ]ŔVXqE@zgP$z=ioc;`!K?]5k!*AjؤB- ^LH!p.&#u7Ϳ0^H7_G$0?iLkJB>HQQ:KBw D.3[$4m2l6✩s#$ zz= Ud E$Ɠ3Ux-JN_֙5%5 y9$<4[:i vVԯ?!)2Բ>[g*#+e7c &^l(BK*s lv}Xi߲x"'m )sLVJφL2u9[uSD=:A2oz-i6u3ƶAO%? MO1h5\F_g d2fcfuҔOKuqҞr3X9;(CU%J7 LS('ESYEf\R(uэ}#CW!`8)Vjq6;U//mh``3mtMC2ˇS#- I`s<&mq2m/gnUeo`T_!%FwLMV%]T ЪVTa(-Fd#1XiJ `zŭbRuwuK/)EYO*IrZ]BΊ}"+wѴ,IImv K"ډ(gydL*P_W!{/m$+_o uzۿ*.1T3G]%b\q)}!/%zP,O6YP#ܑfSPU5X_\{1\aUt_Pr[l3Buq'ukxaǩ#E1K?uSU*l dע$xdLʠ 7! *O*ܜ g|D%̔{b݂ծ{h #w<;1pCۢ{l+ZҏD0u"GToi90*I,Gcɸ9]5(1=asSɈPO5]8 cl#ZAICOJ..*lK#RHQ^I?\S2"xڿAAA4@HxҌ!#"xl#畷H]W::[V*R[[vz2'}~f~mm?y;:=9 6ugD"vq+]h.ofxM 2ņJ7Jm/"HՑFH%GYeUwN狃i?1 1>dFHj[u NDH H&_!?s,& R.pRNAćFlI*[y884cmv-b:oe=J` QΑh4UDc,z9| BWN]*#rld*!,k oJ̊wY`Dx&;1>UtXLֿuwYg?/#\CWB+cSBG#ΔmΥ ZpEu|8˚!-&EqSD"J5mD\^D~ YS0t>> (,xvGOAt?ˀ,̈d3m'$$9^+8Bnqd:4΢R! ´d;m!=':77aoJNٖ-t+/ :C_Iئ>t1'YƜ;e67~*ZIȬ pj"U]3@!.@SⰁfނ'9E덟*Q^!RNmԎju")r#:Α Q#GQ]m96m˨2 u[xAK;q0!W;N56Ƚwo7s 1rML7`/{'`G$;81ޭݤӳtRc*zFX;RZ7qC[:(oP~6c zLB:&qWC8?)ބ !O hAҭw UPF*vu汌lkJ tcp6ejڃS l@,@ډBLoI p8bR5XLǂqC&t^:Dq3_"*v4Q!*)0-lq2_.}4tNLnyb U0Xʭ^Rz_B9u >IUڳJ#:sDR~᫾+( g.AJɎI4ًB\CRd vxwPђz m!XItee. x: 23XB*6fmE%-P|Cl;Q/)qm0wj'""(A[ x弅^Cn5ZZs! J3T Wt*_l\4cdwjnCzZq4di"Z9,И[PRփ;E[G=C:bX!]7i'U/ p,MKIϑUSa_8SAu}(|:T0O̵uM'+'8sAH8vԀ:zkPt gݢ{f.mS6:2 l/B#mSt& M6^ X9Q%uM&M묽dM@=c(R} )}Ԕ:Uf܎n\)6JH㽌WKj8Ɠ!z+:e_5bz{ ;^8W[n()wyy7 \'QlLB 90BPq˼b)Jh`~6x1P A.$3Wh^(`12k0ħ){9F>.!Z 2Iȼ5VZ;sx]UQL O/؆ͮ;EVPZ^yIc%l ΣgʦQ/hnI3rCՀXYBV},,0I-fnr5e+vV^HLA Fhq|jj]Anr&a7`ȩyճ>Keb.K"cywk V[OZI To cȅݤHFϥ#50Da$R틻g*.qm)q Vb|D^؏@+RX'<|ltO,nxgz[G%wV/,k{@ &YgDŨO_Gԧ'GG L&'ug7[x|M:)B!1 gP!A+hsFËi0F$<<쏴|owe* R(DvX(nՄ#Fm-hzn @j`l0 `T+R:pإ-ƼmK02VnLR|Vg 8ɘ<礟8_m9|m ,=ʒ erE笪%/邪|/sSVm|CMPgfm |Qx/z3( >Cg^֎sYMd̓|Z!ȨiĈ팡Odԁa,%aWl֖ܟ& {:xlWŌe!uޏFfmnk9 ņg ZancrMq)dەe~3jX1ℐk7nڢ3NߖEN>Yb~Px9Dz`r&sE)eygrbu^vҤmk9s8䛰Ѹ\>zT҅s1?6ewC9?⌾@&.s&Wهt.smOxWXai^5Gj{DaccUPz;7˷&"lSaHc9yi)XFt 1͋v7RWYOӳR:8sN囶zR~e+ ވǣZfZ)K ǪIx n[U!9v<=3EuыWߏ n?v\(y,'c6R G<_j(vyVb y:'2-[Cn6R0tV?N{;̊[3+Ӭ'T!*,&ja*s;ϼx\8_7dVoD=A8o{/7\Wm=W_l\{,?XlW[v;m MWZ6i,4-UY_l kX\zT6)x_SޚQM[ʀp,7ѯ[1W$Yܣ]=LF5UZj{D$ĺ];L~pœl\.1 AiH[i e_+rkG0 )2gHf$l:y$~Sq \kgU&^#(uU tDOM{Sf]3B?JUq'vxƙ{TPDi? U/{2vZ|H7d =>g8JgD ؑ~+Y˳F.+7\GR;^B>pJTefxĉ;/ Z()cr*/ĦGre7T#tG^92##HQag˔+܃]D˒%SSG&]eόeQq{ >Bk@EfKC mWmG*u.ݗQk 1ļs~}iiqM+xd6s#0GK Q#;]6gh,.ŇS up |z9?₂kF|ݚ p{?W^z* W"=7gBW|\G HW܎h#uk~ AޫGVf㛹Iyִ}7飰vVl'2lݯoԨ^#ZxGԑ!W!}<7>٤GS!#_n9n2X4;/_27w>b$jڠ#T$3{i26̯DALEbk*9QqŽQXXkּ~uT?#?J#sA.T!k[ x~Q=q2dvB@]x43u$nq@au܉\\hԬA*>c#=7ha7B= +k#C=ܣ(<4 d˓ERv  be+QlGOK ؤlSCۍj| -Mr[^F^I$')zďХDG&޿rkDrnM{[  )ePXXb||FNqwu*%~z)7ɡI$0MD4- :y4RyU򮱛Z+wW'q4`ƷNu#׎ 6ՑK㖉p*ޟ;-c?vWr|)kVSk귊Y-÷A?,tK'!4ǯ-~|wGCwiJ-|֜UY]>|#㘃&/` u$&^H9X|'@|W ڗ?BNz}pDE;h̝d՘xCK5Na}q p}=ڡk=r }?x5&2"!(p cRXä3L#)>`Em;6J%?4g'#Lt)>>ϼ혟 tN7i▻="~,}Ƶ$Mnpؔ:Ys{!kw(mvZQnx&l\yg%Au: ,*фlC x}zL!:6:jē |T {AOZc&όJ7=*9Ku Wzl>(L3`o)i\ӆ/x ^<OZc6dWlo/c,öH~/{b>w{P1~ęJw/_":*Wg GADn]U(c0EaCE^-ga_<6nXRG{,(I:U`d^IиUVךsovuN gXlt\ 9+Kxt҃* kVWPEۚ?AިG҃RIqp%ЩAWۗ:iUN7ɻ xx*GUc)ʞ3ꇾѴWX^9:U=ջC\/=0O٭ǽp#{ Sz;] (N9@rZHMݳCzi[5{B~@a8vn-_"u)S}]%5ʇĵ  Y$ԌThh2J,Y 3:0|7+,DzZ(m55u{_J\ y0 CyW'Q5Ghw1zF5`njjHc^sp\h 7!#i8MZuq2R>bԖF~m0rLqBL/96Ng!ez5AK{~u~S^9 @|eVNHI ZwW\xdPo# %g,CEC֗c^'nDodCrұP*\A)o"6(#3U{3<9Y\!葰Zm&T\'}+WSgc1ߵ_Q'c0ǜ,'#1+/uMb&lLm'R@%u{[H.8SDqtm߅M,aI͏"vVJ3vhYвMۼ~VHNLNVFHEXSizY0%G#F#Q G8qaW2B uG]i1y8a)IdU? m`2!1,O3`_t H } [iBJlQs|q$w]Cg hy3S =QImG !EHM+EPtSRAGT_Wy'z {YCV:$+'4ڡ}SbYinmVqê4zYOX]CVP[L?m #[Tbb M'rqDӪ FŤ⍄o'B6.SQxFoXh^1g{rvp`r Lo%˔Q:^{XĘaG>iCOXc_v3g&cOG 0efnAa'Y D hFdӰUP_fݾE1,`O%W +kX܀;t݀3>놲4K аRGAJ+lni"N.s ]7vr<: ]sDϬ]y;2]S5B:KHѶOA%ɢMi#}lSߦOݰ=@'X;ͧ2vu2<ղ0f.y|W*,F"#wʈWP'~a^p4TQT'Wwыb%e;[LӠe^.>; >M.j8$i_!_:xҍfF~5irfN.NL?mrh֯P&;ϹlJAiOӴa*0A#G:$G |:zD ;MM=Hf"yӣ#g:>Ay ʳغTNcvoJkF=}n-.a\?]2-CE\ }YO\QCf^;.?a))Bd{+y%"?сM̀ek3_!V73Mssw?5=;3PX@'6|7.>hOX_IG疽⺔(Hi̜^zlyer?獉+Y[WR:1?7lnU]d\7q7Gvj4wɞjPJxt{aW`Q+:7>TDTu8nh-* Ȍ& U6tPR\4F#W |ܢ6;OPn>6Ρ0Q~!ţ.({FW\Oa)pJ8'R{-sFbHpsZ㝦^S&",=Z|A7`3xl%NC~$TL ?# Ia1ę"! % AC򼷂oX0njB=Nh8'&<]0u3_#^@CĊhX֪ Bѝ|%aR'M0~ P G{1aA)dҽ։bXULQ'|8ϓ"G)Iz=a꩔0]N)^BltAP Fl]ڛ8u[n* vhJUfjOD/#'񰗸o3:պ!JMv3OI˟(fABW٤$=[4@@'ͻ qG[=-Ͷ Nt-> EAx٦ZN{#{:`DjHx#ȤX~(/vs/@ӷKQ1"FqC`žB-n 7}C\>Cd6 uM­@z2I%|o 4,0vf'Uoizka Ob<r_Y2gOT\xMt Z n^?n1P(\㷋g=>ͦqaNxhޔVߢ̞|d৯kOϸKE0pc=._By.OoJ}Z0dKRWIqf{ p zsٿ \€>Ѳ+>Nf4h0gz^пUo +C}P9|<{|b/jQ|F"d >pr `.&+ĸ>,rΜQ~RaT~엾aik 8舓M(}"$()m_/&^HKkcJoHAj Z1 ̔Pl(2O"δuW=f$GX@9JM<*$ jQ `?"1FvoBj' GHu )0WӠ+ȖvV`&. WDnG K`iM5 [i'Zô"WKDXd(DA~ݪt4S"$Bi{ A՚'\Z҈2 *yٯ`@%"PXu͈5v_>`%3 _جz>oU|!}aIQ*^Q4=;/`W("P $d6牐q^)+g4 4)/Y~@jnP9뽙R ` H-]nrJg쥼h~s>G؅ 4_Ws%g2_ *2OghV#ZQ{"=9{J9R*7dz=Df# zPQeމg ~N"WLj3*05o͸Ai xTbP;ķ'T_y蝡&*Q`}e0Q;Db:ASTS*CT\wzmxjE&ZW؏bmS2=~8VrJwVݜ:' =@%ƎL-c"a(AwFwwwohݝwwwwwwwfrl6\R?OJK彃cy pAK 'Zr5rh8>Ց<}e ΅QȁuQHʁ2m?´xV&)Bw4i,5mL/ˆߚ\r’! M:TpPx,.>UMQXHm ݮ|qGp]5b_g $b"chIO(,ekG.8(R =Ԟ@IE QSJw9:)] >,:aB@+e/d']:jk\LiӳC4ihA$j/n~@ O{CwJkxHbܒCFjB:{Oܕ-.1M?zDJKrf1YTO ׶ *5 8أDiI/v-5.ۃz/rn>Z{8iW*w~W @Cޘ剦?"职)'_?<gZ:c0ݫiX+ xx( -ymqS&e^csgHO>G'I*~t L~Im jM5XYZ5#fuVpΌ睏AdR-v_{Pu^#Q&kP?Y 5zo0TuUC`{{ z|o6T Qv-h6 J=hSmUw/w]*KQ:(0BC fI#ÃhD{.$3k*čX2<'K?[ fzmeVp2?zluvv^Dzޜ c ),\ξ?/.:Xm +H,|K߶oS|AEzo0%f~KFF][*F<?7:v+[BiЉ_BiYGO- 4RKr"L amGT\CN x+0^gZF]ޖ'&0]*gTB7XJmNsFL[ ]`ec N_.W#My؃!%0}Q`YR@R}?mSv3i"fmQpggܬW_9$V;}ރOh턀X.*4z;! % h {iTg;P9?>&qž7\JN-}UOq9PƿT016(\ʈ?`,P_1tÎûj]QOl5T)7A2a:w$:|zXׅ\2Io "GW)oυEߨ`Yږ"=@8,,\` P[VN֖%/k5n[bR_̪L9p~-_DQԖ GHGGk{ӼNhtuJKHJG9*op1-)wHu7Lћа?V3fWC& O-Cʃl-=؝>s^M^Vc8CTH"sCyֆ5n !IiFԇZ3 xyspѧvo|m/ZEGg/~8诟,S~B[Qx[š^7\SnK9ZRxOg 4G#u/́} lֽXpp|"ӹav'Ӑc2:W;4bJ~}Ws"՜ X>B:C+<|TMRۨ"Q0J Ckf12('d}I]&x1E+9(v{Z o kdU2V:{eJUf@i񵆙7aeה}g,҃c_խ(HWfbhǠynJu@]jqKZ-[Y_X uZ+ rع G[%T0pQ\N'%tSfyKH+&v.S?UI>w+zS-\[ֈ[uKK;V#_']\ښ1AZi&텧<)`8_T01MOhv~Fl)ƥems=qK fmC)Q%]7"N #GQѮWif+U^e$9sCm7\7JRc2]ة[x}ɶag7g<rJY:\u7dc + &4 $4k!vow$P #txRQƴ{,/4i D*` "~\EYp`RUDZURAFCKRR9XtI%6G NgaXe7E&n˔}S_m9?SfG=rT-o1eq@ezoQ=, {L\U]>m o9  * BH#2X20iG%H,Xu#w.hуl9@gst4:vo Hzd'ZibyPGX PKQ"DY5rx4$kwգ{8>mYshb$q|o\5ˎ:׭iFldV7CMuP4xc6{ !^Io%oAۭw6yZ^bsjq9>ygAnG\c@wf&C̀DF[ĄD6Tτ"zi;`',B$A mzWby9xC:BS(-06x5Nz }vwKʨ%U:Gw@s}wPF w@;Fdۺ0{ g َhs:ނ=kIYcapWK1eG"A8E ](kSK 1xnB߂/p7;+A2Ԟ4J6ֶoJuo9!cDLQf6Cxg2HrOJq\dH|y3둦9 ϶w(kbx~1+pRxD}9.x۱R*9ݺ\; `b)` tG천dߡjf9*"3j^>eD"_{0S=~1B~GCZx\ ˋ…-P pAHADCĮd6/:MͨM3FeR+ 1 kz4dOjlSOCmӿDӱi`,fNzvV~M̥#3^MѳyFXrGs2iisOT)斖G?h, ?\9g_[3}#RyMQ^$}'"?տ'+voH~{t^;H%1_1h!^kI B)P۝+2HԳ«̇6D(Jy\fT@7+Od]9$?lKiX:D jk ЀcJ]VSnLD9Tu9GG9~FEj?c e/7&EZaRd}хK[}$XY-9S;.MIXDZ]^f+WX7Pv }YDs Z㴱aKQU\\z3|Yz`g9[dO@AK,g$xQ pH1ignׇP1yQh]F#Uvt/J5"%IAHa- 6xO/5Vݍ7,@s\|=S!l0ߢ&DH)vX"K5O{BQ#x3ӬOM2ݑlYc`=B]4~KSWh[5S8?eG'Nck1N/˙,˜s̸X ^V} Lc F9zMЧ<#)ʹq7ib="0 i=z+b1w6^goüS{!$UJ?噟zzrDAт 3(i{gc}z"I펐|>C0w x 2~.'Gu(gA&.{~sUHX譏r2O "hW(m!m˄9;D\4 5ǨDuigE CKaE降l0w8?9ZfK9fbH<"02N6X !N%`Η9TSyj|=Y L ,;B ]"E.-[)H8pYj K؁ʸmrޟil@H?C;6xs>/lW1G( `d60 mEH즌WɈ"oDu{o]qJL踰olT,h,h{ްAt=LG9d`Y"uqv ]D/Z> ̙0u!0U JvjƢc[\` gxEu7̊{\1 |k,z-/;F(k2? yPX_Љ8}%0X n2(#ɐ 0 S𒾑t !gr ⡭i>8hBɦ0N㷓=: K %5zT?aX-I_BsLX-/C}tQn!?c 5UחEՔO3V_a{Noe1M~= C VVk6m+vP9GDUqSy*CND< $eIc32|giйERX2D/yH"t䡣6fV,r꽩7GZNu}n/W Ae׷9hzAQ.Z u}&@emsJ=z&/~ןFAb= ZI!EqW@#<mbgf= {6RT6: nmm[tOxw9.] g zIxKYry̵XT΍gD7F)MD#cU/"IDT?2GT h =:>e eʺgG{'/_nGb[hFkUd.Z_AK!RĶbU5$[`&2ۃfSa9%6gDS.$#&Ҽ18 sVpԐL,[#j 1IeܜqG^3 |ݲKyur C45LcFlwg}kY[ducZS[dl8RDbh;vW4"hGAeZ Ӹ;VR A'%_sЙTB[Ӭ ]?p (Ȁ^%&tcA b hQqlCjgcE f]S%=T ateMo$eU'r|`opZ&EتG ѐ'}.o:^$l.e^![v&ǣK 3R(XXɸF p9%ҟ!J4ѭ0OEЅO=LE,6:ȓF QDpDR}}?]x?2KX5vY' v43Ӈ0W@'/R|Soq_(˽ > $⎸Pr>_e{KDj0mM fHvo`Djmq`dg-aә`rV8x;ܖRR?<]L=@Hh P3gb<3-q;碙KCQӿXkaiK9g%k(a%C꫈>Y(3a9REOCbId,V>C,h2Zɰp+7J|[K1n8XLS炐F(c ;PDI+kOAHKmɬ,mz] 5p -QQ!s.lI+{ܹʏ^1 .$5&r0cwr']MX-Vq'NZ\0xduev.=}K[<꺄:aL˨CD_'Eqט.dj>qoD圳Jޘ)(gpfzfTYvmr+osVL?''Lés 1΅xuPwRlZ4Leբ=? K=DkgUuR~?r.Ta FLcJHr[-CUcu&AH$fb1Ϟ6vi~&JΓe_C?$34׆ߍ>1cF331Y69|b3\g%u{0j&0W1vUh>9-mxY=)#.D:j)8zO?V*+(x2OTr:M[*(.{G$ Kai+Hfo؟qKJ 2}~+dk}nV:ϸk?QZQW{7r[ڢs;,dgz>#z@4M2]Q=ݯ!ю&)CJ9p ii" Z\\$S`۟ Gγ@!-6b 7ͺBxF6Ks)ٍ eYFcT1Q"jɩQ'C0b5O圗HfG]vOcӸ'3OӃPZ $<8:8ǡ)JsܚvebfqDNfCYglffq[zFiwwp_ʣTD"i!B[}Y l!&ϖ;NMt1&^tc0 ȴfEOMHjfqLCUMlAbz,Cwz뒴lD=y_K`^agz:?Mj2% 񀼿anՂlSF)Wh #/,͢5S2d&8.HH8 K2>S*n%ޏ;c}w ' ^ǪBzη)X!MAMզ%,K&k`p= ?ǡ:}lQIe&Zݺjv;MGXUFr `_+ E::,HIQN8ѹfL!=W4g[I~§rwZ hN(8"hɒg;ϑjoa ~)GpJiL ߤYNvS2(dD\?ɠȿe76 iʫXړK tJ1iok#v!ButhV/p8OzD12xs})I/QTV.ul]icQd5wY?(:.,:wMĴ]ǜ\^fvBwI0Pw8y3219Y=0!>u~|QkѫJ:-qJw\}ѵo{WY.uG!(G˧YyqS4ʹsvw=m\QV1]^ώݪ66#]籇q{iP'X+wf2[Ezԉf XfequΟ#pӉh@R^apܩpm(^14ql'#[ö!i|Ce}Zu|3H=xȐS'sxә'APbq],|43a~@$i&u9}UkN+DT{G7H1w^Kq苍H<"TQ _s"{+P59\nGJnnch<ailBZKfz`w_|ŻDt!CQux!(;2Q0&9ɜGZ˵xbFƉx~2㏫!b(tyZ{%񗂥hlIV`y<%dO !(B6T#ҟAUD4WOA8`O]K{xMff1Z:,w Jݱ~sͻ&Qe591+i7p0#}㞤M‚9+?h `qFy++HWuw$oo{J01~݉i>G~9iRҠ]'[4WQ)y5UIk)qi! IWQOƼY[:[ S^Lk|, 7ej.1\sWBŽ"AKCG_T IV5PrW&sH -Gœmo|zl6uG3$ #p`Q{zc]+ y6+ӍiPpFŨceR QIomA>m<\KoU?lpH46hΜ#yh}L'h%0.bnhZalca~[ngQ6297/%l%"4|5ԤE p@*JU8)5Tӵrtb+ )]\i=j7֊W#=hK#fZl l+ .<˙IK27YtwR28]MjW 5WSI>aikG/TP}q޽XZMDit#ٓ6s4qll`@,,UJdQCv8PtZAw+@lӶ`tح*L2Ih<5ܫT%j*lm붣VY,t#V5./j+S\D/ٗ^&R1!E0,lٳɻtdnFTUqlÚ467-8;S7w+B*e0E3oFET-ԝ 0Y3mx 1uDH zWI+p8cw%QKH+KcZ;o|鴻1o{N9wwK Ē]LHE~Y$oORs0{dΉ-t?%F6q]g <$˓BE7[*pےˬAl jmuфӴYTmz ylwuXE\L:wm -OXI ]UMjZ"ѻK*p)L45,ރJdҕ851x{l)AOJF-<ce9MK?鸹9/w8|ɬ'3DքNNso|Οwü{sÎ ͎3 D[{xUԒSIP T]Deދ7Һ[imGy|&}O:' i[US>iv2$fViIR(*a#7Gc)"^Ǎ2RIWA`)AsOKd$љ8vSel]!'9.&l?`@\iN޽6ۑk2֏yN<(! Z\|HHH bogdwS<ތ@a$ڰ-W#GJf3(OI!|e& z9vڑ qQ۪:L=w4 g[dćrA\XQ-H?M.Iΐ؁ωVp`|~]];3VRlts0ӆP _Kmlx~#1֑7wgAQ:6`bwcs֡Fls.[kKT9u?!(KX>m,!Ŏ2dĂ*5GݦIB&6`56Wf 0߁f UrucmX>8ňEwĊp0ܥnsH#qxlY xc)Vaxkqf#`fucWkư= !Q@Y\Xzc#d;n6ؖK~װxk0{`V hZ3Y؃\/_r7{E8|Ҙ$wn>Yt#j}Yfe9UJZus,d]u 7H3=3SNIUa|"",B j=B# =k.#p7\pMFV n*ֳ$ZyܧT-"υԿIfw9]f˟o5Kq`j`A }n @- c J)3{3I!m!Κlʚ 6A@b:-x*\?'.툎3.+{aa/ vU!bg` 08B9"3QbKǦd(9Nۊ8aiڙ?i[kUIEH`Jg{Wnb ;-7t1h^<ާZ bSp.Q .ocۥg*娏7c%?o\=]ֶ d?f-z. N[[|Gv p+$0. 1scT)g{,5/ŭRKxc_H Tpj@ :2pj@'2'!Q$lBf~m.8!)7ۀF+w 1mKx qČD?/,zB F^'%!QzA"mQ~YflͫwquCT+g@ @)/%?: Mؘ]m۶m=m۶mۜmO۶4q:IUO*Jcŏԇ ={3Ks<@w뭆QEדR %p(C)p$eR>$z5oyomEdu3>e m`XWQuuyxkҰװP=6cz?s>}O#X~QS.{MσҕZ607O~ԋSq=.x`!ol˖:(2^ۥ07߭tPYMPl"},!Rw7 ;'6.R[ZF5_g@;\ZE|sY5ࢻhϻD`RnW3$Nė䷲[ђM5R_G rwL_Xq 'Wꆎ槗+ f#r'=<)1ؙTjLKH"" 4$"嫴 ë4ʛHj6O3BܴAyZY!x,!MM0"c]C)i3F,4a3G8; C]^pw|H&#Rp^1 Ι%;# 0S,2( 2۩Bp1&E}1&SW ,o""l#ro]ȅ(C y_' o(&Jy 0{Q`[1!a/yz/?@X"4ta C|:bVء&̏ Ч:c᧞~!כl'ŧsQb.ɱ$(ǝ͍71bkWS< gOKsZ}̯?jV1q whl 40.,qcGoEA-K(rP L:'T{"E3[[wF,ݮa94vGٿ#%7 ;I }F\O w$+:b%"+ВgG1bS,2*.2Xc}iL^G@G%THIA{$ eٲL#T:TϏ1ِ4]oot0W ͡e뤐}ir{Pխ=5-`Tӿ**?}; oS Fǖݔ嚤8;n$5[_@߸`C)D*æLkh|Z^@?y?'lfBl2Zͮ4*:GE޿e16Ԧ21<l&$l4O Q_G+np 6)޸l{:`urGKd>Yqgz` ifِ4yOɥBg>gʒ3[=cz%FEpoPmx/BjTiCghVmA$gqƀʋ_y|DTz h8cQ'-Ѩ0gđ_?'ع?\GMHxJ֨˄3$ R>Ta<--@Eű~Id4 Y̠/oW8 A*c0:8MT9Mk蚗ln6+*zpee_0x$R c&ic9qq;@mLk\W"; *yrfLu̸4qcT2hW#1_gH1ܨ O0"nV 83X1vp +0 A b̺NĈ,i?Ľ-)} (+xX7g<ȱ@!HN0#siwA̭.h{e&Z9frMn':?B,Q~\IP ]}z$GCH/*QҪrO\li&ͨ l>Ӧ,5mPMkrMF,56i׭crkֱz'VH2Uuxa9[l j˼f]88L":e}q=x髽Q;hk)5ρ~@ن#tJ^j* 꿥?: BlL.&IrM/o\wBG9otS3%_ 0X3k9T5FڗኩD>4=Yi_B;!|8Z. RZ?H_[\!KUDL3633ynju/=N|hpէ 䛖YCF%ևGeщF4f]\5d f0%@•H@6 ]zwz7{t:MD {u{Q {ˎ-@cOc,I]2KK:LQdwXfGbÂlioi7a3oq 3.u o)Ḟ׏$' o@ ծ MN}~]quY*A-v`0 V GIe:#KKElq1xʻ>~ѹ<ԍz@Yg;-_%ړeY9py Չ(Mq Y5SZhfXK_WͲ!SZ @Ts~>&w}Lke=:fOS5_ Id:iǰ.1][zOk0-˫YQ0]o &T`-Y8X*V04οe#c$6H^c<}nS-y)#]νzP6'gW )eSfcj*  2zbrU.d;i|#yԻT&5akSEU;d  &$]K6B;I/9#4*8B6=p# DWs0Gl{z)r.AL%6FA{ɼT39Es2qLf#_*%7Iv*fY>{51E8IPV S!S  a,%צx ,MkS6h0\F2.qiĝKa3qo *UJ{ΰ,Ǣc@ {%VrdXUزh(]ݳ\#ur#Z4) CNO f&ej4Pãl[Fø JbSs..'EG1+"sP$)tD[&/|fb8.^8/R*ݣi!@IÁ-C'D78]@JzeQ5_Z9gO%7]m`3| ٕ@ b!H''! AbQ5u$DB(aOƉ4E`R4%0d:eMP.% <. Ba@E=#;7*L*>V}jUmS" 7Jń%RM=v-À-ˀ-̩B3@h'O2P?Uq HuM*`O==RЧY̜|4tIקF]b{yv ԩU]6HSЩqBCq[%O~ ;wУS6r4̶[%cK<ڗ\dwӴ#[*:NH֬{GG,-*J^=/XX[w!F+n}vhn(lO2}l7.ʌQVS q7b FeFͿ0[3|;|k~N|[V>=Єup ZdL.҅lI^ۥrv=^k0K&*ru >mGvr|= znr€6ǔE=bOSگfi4gOwEkݝ72淇$VK;H(&59~a59`5r9Lag )IPl)$~6ߜSopUס/aC7Clo0VabrCg^,)_JQ&KƸ `⤛wG`%SmKVҐib! #0|K \HV큶ˡgY ~6o,_&):$5?E3tnƚKZi'uƳ|pf ǤF)}oY%wǢ!AP]{Lb;~5u4w|ھMz{N)sIQ{CgE] wD#m~!M'tFK}= 3圳%v YT*18A뉢ev#9(:ş9Aķ@kk%:@BO6=<7qBJN 4r;hv;2 Z|b`pxO|I`wh|IkRZi([fm?`5p5%7b|$0u&>ώo.mIj^³*i6o48kR`iۂvB%QpjQԚ;3_j č=5/MH/N`VCAtl.נBš)РOA#2R3X ^aɞg| '"jmRG^4jy6>:KqW񐬹;-*o8Zd^8_j>q fTys9A8oſTn݊ԓf1 YJۢ v2wPjzMDo,1o6où{bФv*DҦ򥛋}eVxOypIuB8L<&ƕjtbuD܌W˩L2śH4ȑaF |F!!h[2-Cƨgjpf Յ}tLp3YG诬HIzE9TJ;`|z@_ 88A$ {砜|$sLOR_ĻNQAwl%'T\@"P@Gy6 Vh1?нG4s+&AۂزbߌzMqe7Zhy2 Kx 8לpdU2)+|cDoԵ7#L=ENdb^ʘM@uqw`Oq1I‘AjE,EGdT`,/A<vנWps=v Հ.#:x4M)<)]Tw "LȭXoL2j>f*tS1!3+}c`f[Gbvq(5o(ߡ8?c)9>6jSѽP ڝDv#O+?iE9Mbݚ?{@jroX2?SҬ8GE1/J^vs?MytCΓׁN|e x[С@ &(oUI8;2ڕf`sQ&q,>[8 r&Lj9=Ʋ@݈ 6SIIuZ޸3~] 8ԡ7|Iy!M7D(+׉+"ãefl_jIOY%Od6Pdn|ArWI-fI.[A1W-rIbS|I.lUo|}e?S汖Ry[4ˎEjß 9oL޾Ìϵ+&.}fڅz6Foܷb A-<>[N~][0^G]z@_+#8Gg‚:ΰ9>\](=$ UHPVE (?IIBa#5rj}5j+ y_4xNMӯ5_F5z% ^U肕 UGRZ ]*J5KxA+*.`K`K?R9Ѷj[M7B4{l+{Bm(Ť$)׆HrOFbXȀJ|}E_9-)H#,S_W??>G(I1$; nnTk+_rKB.xywEBu/0Y:jhm&hAuy""v Օ 9<ߥEy<6WX ѳ ?>2f"X .(U2W(/[l |RZfBz}]k~uӪiEd8nW3nH~_lk`{hO=M|%gg0[9Frj0񫠍E#wHQA?9ԍ]Eפf"xTt<$m8'T R|dt Vqb(o G*>kMK_Q@ٖS-* ו VG tR[ayG iES ~fӛ*Yu"(L{VEtlT=(9 bixPW bpQ ^z-&PPCYdޢ_}2Y+_\0j_wOh| t-=!`K: &7ygw^(QV^ ,-^k7a9[1Y֦)N/ /fmս^RDžd?2\9 KbNߵ[ FL>epNas#=wMpFh_WlOu@A8ܡ^ЭdkSI  t 3*@]; |8rm}o؊8x!BFa sX (WD`g JK^'vmy_`B:ZeMX]j[W3a1*[22GW7=n5/};FHw_m71:ƅ4ɛ ][0ߢ Y׹g ۪&q/vĔ0~u9v8F`XZ.+f9С:sKEXjV>uXCPZ_u]P(=u=NU=q $8V>#@ie3.y}R┒DcnC&ٻ'ؔ_RΕ6!es[RCoD6 {Kg7maɿ]}Tۋ,5^I7֛+kpk:wٗAA6WH}'PÌa#q\X0(+&FpTȭdJW6yvN» NHi_:)В'pj` }FE 'Qp # Ս}c?_o)mkkuq-\CoZU9̽ s5vf齳"c-fV,Ho]lۓLd+X-[DDD@P W~BMu.KK/)i[xLw2,iom^=f0qexpK`TfU^-?kȜ<35㦟-ְabomm$x3^dldi\)v}r}6 ՌcYU s})s J.XTOV~鑂u${뙛; H7OKѫBFSfA@Jz]T^=װS TB۰@ô XrФ^S[UizE6]$> VmK Q$ĭT_ŝP;Vv%xC9̖Y)\pb6,7"qT?KyxÅDc'!Нcž9_$ os1K~(h`;%$tSYٰC 1!%a ^/Non ׾}5/5  8q"$aBX]FE8<EU>5]QKHhWY} 4OC4_M{ $%|3HD0jɞhꮆã}xaz "DgSB5"v AaB7!{W'zVm?9Bέ;ef"Uh0ٖ#[QZ5KmW k ;ϓ63GU<KVS'[W nySɾkۡ #[Q^yz7ffLSJB౎]I3q^jr=`\곷 •˿-ͽen-̇Y9Z3`[Цdr3$Qg?a-94yLA0_}Z 8)*4oo.6L% ΢vݷ* 2Hb(w+$lWXWm=a }ƽ1+sIA]HH23['k[DZVO΀e 74%R'c-ηTÿ 1׽Qw&G4R_xy-5#K9A f6q:$HThMN"Լ}"| >2T?|Lx'}3,; l2n' {9imB&( Yx__XזI8C*  \ W\g+P; yo|>2WuMc-O)q`Ӹa3rzWUk@Ltvu" #*P+rY-S8syHȿAkPSY2J8YyLIaaԁSxXa½_! }{_femIUlB`J}R͚Vd9d 8~;ܠ32WoU`q ;VI)+ZduK\2DjG"ia@1ĜZp\5Zbl 5P$oCX!EkL8fYݝ@dN:삩T$Oڂ~d]p5O^[2*8 (͘/KS7}[Y0ySO}ǁ틿 0\D>x<9rtMUU J>0UQǨP^UHk91%ÝAyl:Ff[DlEd\T3mO[Q,=nhHS >< P㬒?7Lv=r&Sq{ gk}Q+\SsͺoŽd0#g8TZ7q# qlw4fdjwl R_&JGҏyH.}ӗ\!'oʛ R}tf8)m^ 7g4ӏOK M l^vP nߚ}DFu}!R/tbKn}ˎG (=r: ;ÃQR=Zv<)\ЋD_ q,~Rʦ =+V~baNw<O þ Meaǚp7d‹E\, :lLpۿCF{-YVp2{J''uM'w?Rmsbw6#yw ,}=} mrBA޻7+G-9Z\Gy? Feb4-m3@${w5JCP8-&24^7ZT=IMz`Q2kE'Tm"xX }NWH8):5$&))0350U!]狙OV?0o?CD_eL^sh$[6̵}BIS ]/m1p (vE[T\7E8K U *Kl#D,}j mG@w#5{Ք6A7(OYA]^"vG~t)B5 bd(o6.ٽ nԡk m@IfS§㠇y9d]jx;&.Ogq Kġݳ\[Nс!2vDS~DG|;$Pjy&ګ^G['vTtx''},?Dg;~o0$qAp丬I O4e\"]L.8댽O׸9=$N45 9u0j&9$2\a0.Q%ǩ$7B~Q.`[U\-Yl[Bù\ANbv ]K\Y&QFU3C;t1qM!FJM4%r-kKpFqrC 9ZOqSE!iKXU زc'GOͤ4ꈤq|{f8Gwݏ<s{ِ}WWu .-|(.ybқYnՎEj `,[}V FdVA4qӐ8<:5{W:\.-4yT18Gj6pJ`z{tyD)@[s*|UÍGه1N&0+uu[E(,x0T:XYaz6uy {(%#F()XYݛQ; ܁ed&'j9,W4վWFK$ !BrT{BiZ[# "Z|lZ**9zcW/4EX#;Hu,%$ 1Of&&Ifm%V,˾<_fĆJnӤ;`FO0S>u[Iùȧ;"d DiەN.LTDvtsBW%+]>bk0%?9%P9_v >cMZB)nML~7g:w$gqjD"Vs ?mQ~gz9b<$ !C? ^+iqi,h*i$OspG̲#;,wRwN6js}.`ܗxDdg~Tjg5' v\rɟ?n>ɂ={ *kUؽ Qǵl~*z&%-߰)ʔ٫UkBdwȟZH+6qs{?5ыl<T8-Tki4m QvרQ,h `ƞ,8Rq%xp\OAѩu~8|N"K[v<-Dz/@4p)[0HAn+z58ˣ\Jԥd5\ѳw9#zr鄱 :,S|i"_o覾Hx_$m C\έ_ /@kpϠbf .K"ٔ!~i˕$qQWl.15qnMD&1X]o\\K;Ym, s{댐b#iIrL]5x4TRdpz99cߢh yl}~2P2J = Ϩg߸B 1إu!wM4{I9KߚZB}/#g˴OIo0ZYOYN:@Un+đct)1>M+7n8&EXɧ7̦q)-cFd324oҮƒ Jeeq}$*N*Cҿ7v>zGT4lM<D4I1a6Mc2R}: 6T#8A]4.{6o?%x\ ݒ*wFcvm7 t܏{!bX].>t̉΍eEjzPW졓N5rbU$;u}AU<+diܓwI@6:qx ʼST Ř:3&yQx7e4T f&Ƞ.I4DB;OXĵd H gQPqhǼ׆U~u\2Ӭp!b- s|-QHwI7Qh )qFÃ,/3 Gp0GB =n守owզq E[!HWOװ`il>1ϔ3]1}t ?u)׻#67j;LVY2\۰ܼf-VY8&fT豻Uֽ$mrXs;%2x6"S@ا[.c@xh"sG$gkFhrCņFdAOPFgiR]6IXÛPhsK&~q#Ih6fe,IȞ~wL9ni" Waqd)1(F~D  :jP W:&0͉(N198FX|zNMiOɍ[03,o6 #r c8Ldx#6oM]L:x0hA3j鷺+3PhEy`qF h=1bnްúGv+5XgJp05 Gg&twXQ3b˖s+4cdN]E/O/B2G F\ޖLE3n%1+d c.'%yn1-W抋Mjܿ^3 l]zl!ZENw}lӁ3{ zU5r;j F(c`#e3)}9MQZϧ7%B?Vg!MފawDv-'hB&m6RkT2Yrcêv+f\Uf*nrTLBR7^H_^n({P<7o/rt]VjT:mRyHJs.saOT1T(tݙ!sH#y)=Yr,S%rc6_v @]pQvFh"$n#֢WpfJ$i@v]fO%7:PBhrJdy֟Ek~9c{dsZL*"O))R ,3L"*fsrf~( ىb{H ɍc{iRVIaWQУ^-F$m"&l(l'F+ۃ#ͮ;XބUXޤ7ox1#2Ҋ29.;5j^;eLZ˅!Osf8i,-ul(EL/&I;*@r+B\=gxkl? =3HLkO@'!js,qXJwb2sXͨFcpDzeNqǦIy$0Puε)-DʈG)R<9(`,)1`S@0)hMB9XZ#iu7*r\TifācְEj/P^S l޿H'FF4dPf;|'v16ʡOz8}ke.!k\%$͜&^ҭz2ԎNZ&{vٸ=9=IqF/j5Q۵tl;xgf4TKaӵhhTLPl-{U3]$Aj{ø_ߌvMS-ݮ^M,8A'Z! A@0XSn郮qDUڱBb+&v;Xl&J Hy5թ8uq1?1/JÜ}8T[y+v^sE?!4FJͲK[p-j3Z6^;N6`=0N6]7QVMb~rld9"kbXTԩ I\ MX12~Nrc,(Ϯ\AyZMoTPC~Vh8hp\t$~약~DC$u$I2SSY=s6]ع=96|)Kh-eSWMd*Y2RBͅVtV樔]{E t|{$uUۦd EU5ɹBJ]6.!%'|J-VfUm.`vŴzQ)zM2v&Pb s- ~MvŗKm°OujeaܳxͶnAPHv>x|%j1S9RQXAm fR^j0ߏutcA~A6 +Z s K}/p﯃IM@00j=e,gs*84<&wi1$|7;o.\p΍9 Nw܅<#SD։O6N^' SΩI'ݪIY(r' v/Fk.aAl-I)%I@uFZA"0-cwZu{h윯 qqr|YQc6>xuZ5ʿ }Zx<%z~c֌OϷ pe05 FLk,vѓY0\EIв]ڽ׫)6Pv'~rȮG)zp[s[gad,{ci 楻OH9;@G$i!U}gS:@c̓3a}{"Qy˂܁t^` g< .|y" 3@!F# A&F%EXR\ A̹Z8A@WGCWvӗ$ȑ ܒ"p_rA:~,8W_.Wp\F%V!F uZ:~d@,CeUE [p{jʄwZw(U-0dJqЫdk™md$ߤͥ2cV+>LAFQ{/nf3eC v-]µkpSğM;UncvB:ПܰZR 2a򐢊rRih u9:&i tre{ĨogB?y4o%m~B<J$Ź( 0f3c(QҀhLel۞g-TT?"GtN tnds;@UJ ض64${Wޡ伺f/.3}>'Ͱb=q,H\f[zp{$[TJWIU9I]e=ΒseҤmBW%=?RgQOK#"W0P^h'}Q.QΎ魕; ll#v:Ҷy#Ojڤ3kWMu V_׬ Mr$C0Bh!`* HirG:v%PV@6X|fP:` >`sT:.Р?A̿SZ,=ajNMf_g*x<{VX^x'ot~)0\#${f 7ƪ?-R˧=r ZU ևŲrDloՇRaS3ںĢ_O!A%֔g 6?«Ȩo^f_xdO/^ޣérJ_u,XdS ֽ ֡Dai]KUL.[".a舻u~`ŊE\PS4/^I.cb˹3ͱ,LY=W[xr_>YO_팛*JE{k?x`iU5]K=օ]=ؾr{5.*ݹe,ǹԛ> oP O56JTO氊so$.MJ>pO!}&h!s6KPH%(u%lHU+˔@#v{Hk\YS˖TNcH NEF\5Yvc2NTr|y#(s.џxºI~a~y9N?7BM\<_ՀBV.*]@Z3XLz^("(tCρ|ؓw u-#(.OOQgP{Ajoʌ*x+K_s4b N~0&བྷ3~)!M.MH,q o ZOlQJ?XH^ǨAp9߇XJbY9'|T 呼n&֒>Tz8-hTlih[n &W lWT_h\j)$O؜J[t5Vlsf,ꗜV픘Uh:4lꮗ ? Xv %|0&N }Er"2.o1cTxșІ\^Qa{zLWBe02)6V*D?Iv߹|퇭6iBVoaQX"b*^":wr J5sRBywȠ1 *DM{u(Z~@q㪻D I r'vꊿzZ/TFqUq`(W۳fݣ0=幟R$vmz@5M!h\$&THF3zxO=|$ .jIR*g볃!z2N^˱*4$-Ƕ/M.\KuҬ۩/-f韵VQ-[c2p@;tS! eW"9CvVF9.ޤ*!Kgp̠Mr` O2\vEJN2sL+"iNɼkA5͌pe.}5r&a3Ĝ;jXQGҸ"4lg 0/J"_uMb74#SMk^.sfңmAw]W]?&˭<)J0>b2J,::cIK-&- wV,fՒJ?}ȹz)xcܞz^zoG'=NX!?! =p-AQ$`~^<{tsRw(ZT# eU xAe;r):SDvF{PmB 7y D͸^-k<~p%U ]'PZsho9iN!t`P3IŹEPn$~L67약RSk /6;[m B;= sEN.i:(Hl*-  gIà6yO I ;2OPg4L9(dԈ7Żx+U+^IUej4M(|*&]7Ns6O_@A%EDԲdņ')}+N1blկNڃ]X/`A4٪ JAd~=ȬIQZG ^x vy7wRxsSkYxfNJg9sh9=ŇMg'^ξSWn!3e ll4ga|@-7e=/ebsPy!['ߢ]u`o]v"=md{9FcmZ# `LT|Cҡ ?LS&,FxB@}AOD VH@g[K${/VPyϥ3;qi}yP {d1?Y x:0g͋Jܾ'&+ҋOTW+FM K%o`l"Dv^l6b#`KڣH08Ãǥ~ $x?c#uVxWl&i@kLaKΚh9`A~ۇ5MTHܳ<$Fpݜ@@)fa8:u(w`H&_+o 1~l+ F}3 FM|"J +$I%p}ޛ0%leW R 4~XO^?}I6#|ƊCmKYgI ۊ\-^ȇI˺dW_g?YFoCG¤=,jқẑʛd0]ٽ|CGtYieWX~~b1=%%u0&x2$0&d, &Sj#/;U΂.ŽZT~;YԗN8 P0GeBTMcE0G9*}?cW? 1Q^ă2dtJǕeaH\(Y zRΤ`;dveqd|/->`{؇N ĺ` ن IPi# #[MȲm3(UbH4Y\dZHq(8)~G* KO IP, D#Ȧ= k¤MRY ϧazn$R F+&"E2HSlXJDI65G2!9%4(jkn3\<|_]MJ amPȖ;atoYUQ)o3435u(uM'p]J&.h#`e3Zbqp^s)o>’P*ɁmkOF2Kf}SK Yz’d_:K.0a'B,lc{a ;gUpɺ qpVG-`H>ΐi!;8'yܵaqx$bp.yK eF}[A2j:/}}r#pAik<~(-r0gp hH%q\;dݖ?MU(ZmƓdې?2c3Qe{Ş^ʗ~my%ÊOÆL!^sAdf3Za5 Nj[bPڂ@9xVm7_4w!XFoҰl`Jp+/jgDrskX **{ i4_֍r=."؍$I%Q; }K>3>nHj -CIVG/& 5#sRwD7{-,.R0)k4.ԏ-[:6^N%ɫ r rrRWjX_!-iџ֌$$j:\?lz ȃuu+o&ڊ@?a {swDە>LÞ>yD{Up6mp]?k Ϡ6_> p!,łsלHa[6fG4b^A?\g،ω^mIkp3AA]+ݿJݦ6_ f[uoa$83ʫ8¬dƬV`fe0%"}fa u#Vqmp tS7̴mHUc[ #g2^;0,j<܇1/UAa V'lYuL)3RcR TA=X" 4 3a4wq4 [r0pe/΍gyhw~Чyg4oL xO1;srL`Z}A1qlg°?xv8]$3*~#|b8\ȴdIOay8Ffi~ FCS=87cF FF+e,cƿpg hKK29 Y>!##iѵMeWL*۶ Zv~ۉ7/ϧ *~p_D=-4^\ җyA6r >7VFz+~߂ǩfMYoK?8٦H8:u>9oP|? gBo*iCq 166j$9r[&[76c).,.`/yG1G7f|k/qwC$.翈17(>Mǿl%X%'" *6Df>c)m /MI^+yyeà"I"&3`L[lx v4V> jz@U(7zwm0B,Y?9ro?7b\| ]b# ""\SUBQ =*8\=0v f.` U?{IWfA檪gm` Ϣ<ed#{ Fg,LB~NkT%;ii$8Q$zt۰4K<*D]X=ҏHpF?ۊ4fcLN\\Y$㗲(pE6Y=7±A$*@La@Ăkƕ>eL2gٶD#=;;O4I-k*i˱ 臀3xѡؐ3נX]r"^sxIAv(߾!c[$#(^=>hE)nN^dHZsԿVqS 2q$buK,XT! ud1ѧ#iG\ɣ>؝Bɾ;1wK&í5,7 ~5Ytqo[VBAnj䗴=q>*KD7(ϧ\ D!V2!P\滞 JZ)MB&̥ߦC@ӏ YLblM $7}imHAzf52 Lsjy ӦZfȹCE/DqBgL{ v^NʍjonܖP,+h߄Cl~qH̅y#PX?!=^?O;21#,WF^) CwtT l,?i)Ƣ䩇4lUҟ{mF\#qIO%hIOZgr߱ӒKom?]{=RS5<&&7i-Aܑ5 0h@!E,}%q5Y 2T3!ΨpG Kԭoö:8T9mևAu|,(4jtcFWrMEJ s(y=Uuzxlո,x<0]X/R=R"\ܵ;bUMVLm1$Դ=jbEp:w7H5GG7Mؠ{JzEC^SD^. l ;Hxw *О[yEx@N}[Րї1$U4"*M %cn\?() }V¡dͯ4D98k76-F$Ke,94{~r `Z{a1=t:N'\/aR"11zgY'Fr 9 $&L{;&"HʹSٚP F۬AnOFsCcJҐ W҂АA^Nm} J5eW 6EG7ZW]CsH5]FAGfAM<ϥUX<"M0`ŲZ+L=Lɉn0Yv?Է|2hĎrJh*9-M4Q|X?8!YX _mvhX#)ϊ`''Ϗx=T8<<YL מ:|mV3,V=zdV6yRWbN_Fԗ2a{-/_=Up v<]#B|ޢ m~ Z0(}Y5~7eR (!FĬ|rz$ R\=d mQeT8xտ3u 7yx36i660ْ d;]֧z?KAj'g"oس1zRE&v/"E7|Om.5f0JYmSݓJy|y)MW萞daMl^LGP5/Mlއ29R8qkW7/VEŐHDBG<#TsQa?C?ÅZ)w1 a[1&*w`x26(Z'YEZ H>TkX.;7Ӡ#mw*);#Q9[yj[JDdOޭ)GwkOI$z'u{qK/ 'H1Xa1Fr+C d6-)\6Ӏ, ЖOϞ4@j1bq(ŽrB[8*٫Fp'ael9P&+橞Oϩk'IG[ЍHk9xoM^t;&WBxTWPN~-O/c"l֨ kՒRLȧ+i F>~M⇭vodp@l"\]S2@pM:W[1C5 cV6 yNAQ6_ܬ6eicD"ݵl䤈p͠[qkHph`d5U2npU R|'{=?A#Mø(צl>T?XFl-(`,\OcclF,Cj]J7G_T]ų |,*paVZewkVGQzijqWdZ9aGP Ƣ-); h#=T42gT*5y~lxhoZ+lh=TccPb&M GE"qWd`=Sdn:!X>[gYyAEC;捭И$V+z(_C(fﻶm۶m۶m۶m۶m{z1YTjWT$_yĉU5- : w<)֪kukmǝ5umdv@Ju=egz(Ɋx["ԃ7-gh-8z28O>݁e>n[z *$*[ fo,xRPa[Wsho A~qkD,!kuoMV8F"%;8W a4eQY{b~{_1m Q =a:],ek$zn#iNwNkW~5yV od@p5eq't7I8(oZdeт3jgbnJg`Ko̎֎+(Bg;O"mv~=L5dJc]=&ƛ~4U]ZFxjhme"k2n%}~9.O 4!ЁR g=}iY9jЪeKynC8%wjzY[cXE{\69 bjPԪR,Q/Z2Xލ3D"@T)*$(6mn$SRFDZf;„@2Vܲ$I{ƛVnu졕ŭUEBod^c2(,6i jŪR/M<&r ì5$a*3 4SpΈ7F Ah! &9u#:NVCKơ*ҭQYCUʦQ MmE ɼ0&[a톼i:ѷJFѣoYel nټm*{'+כXٻÙ'ZRl3DgN kz!.*jK~F4v !oL=Vɬ)‘?8ؕ( ۴ ; >:ћL^>7X/,,|l!X.bn7Npo=H{`<4N̗d.Tk ՞; ףx#Uf!’ALl AfUC2߸Pc1ۄ&*|>r(aw:Rԁ,[;/Fe5y=W?]"\Ig]<^Ni k8 Cygj=!B֛bג WZH׳`v,k2\GAS/A`*4$dUk-NB)X[ ʙ mW2 `9(4nAN.RD _ې'i`FTpz.7"Ly? J2U콆@U+cp?XNZI7Yч**$$d1΄oJ uKvUhў׺M!ƶks7e7ƥ'ݥɇ*rUsuJFꦧi{}_:R%uEߦ=jxm/ƻz}/-j 唜_, W][]C?0;B?qSI_VlAohe>lZ37n){rv)*Gh.*7KS|ʓHEٻD4ag3+4EnxrvVZ.;c6HE!)~}{m»]jfDxtJ$^Me:ts0='k]foO'Cפn2A4 .5 Cm~xzgBzv3ȇidی~ U {$&ۼ5g´8Rb}h;.oi9~8*»ze%{ 3@ 0R 7>R8R4RñHf ]2%sdAfxlɸ7a@[I=5/oϱT穚b[a/0$Oݚu+B+bְ!K}[3B_珊7ff8-C姲ex 8}5wɽ8ɽ)izⅣsRIKd'tbPoQ-4sETgM盰+4J\ccolEȾ\}gxi=ҹkEZo@@ePؐEjT^d=3) qi+r j(Vui;"v3R}Tzrr]Q1@m1@~'66iZt_Rc6a]](mF.ٱrk/ÍQuFMV n+v/Gx"S|ge*evR LEgFcIq(Ǥ,6cY4\Q`>?k=*7[Π PZuQW8`=/kr$r7MJ3ûGMEi# jAJ6d *<4!-MlMܝ l=G ([w6ͦFJCI@ f`SAW_j2պm/Sy(m545)'lvvNO/`[TS@V$AۍZ=&R8Ѣ56c"Ž'l1I "+: MCЭ[;]o 3 #q-@0PɴN\>^Mi,JU2M Y{n XS"1_,YF kP2qQ"3 #4b2 KE܀ez0B$Hs ٫4^<#ֈureTH+yGvFYֶrC}x!\zFbŤҴ&Ra"4r"j,ԘL]b6' gʜcBbYEpǼf1;-_yb3TA7hW6Iy]!7'7Аo_(3RpxCܺMy.]UW\~I.b&/5:a#g?3',Z汸jNĭR"eIɉ1|ąX#3^<6-lιg_.d왦cAb0_>`)pook4!ܕ\~`d QЃEMy 43+hFcfCctЌߊa$r]2KcV-6-K$4ɒӿzي0,}kTIoG\R񈂍u4# |`om9s!Ζ*/>Rr1Q ]xeȚ,̦W`кZ-c u)Yf*DRj|1ԆnU_b`@2gD)Nu 2\8J@7lA'dݿ,/jT|Ob.#6RZ38SB,Т*-2 ߨjt=.`lV*,oB۱ eAA򫚁%4W&^j EPXaG /Tx)w9hbEٙ)[PMq%iN^ [Wnۈa"/;l3W^YK;4ܷ!bd^ZM_uO(#W~ĥ%pMTH Ǝ([ϥG" Y;uKlO,RB{v}nɦoJ7=%)jTfN:n!@aT]}pvF17GyIqYiU.A,e=|Y=$Xq3NY< Ec O:٤A,>DI[#4YBJy($YB=c(66;beߦ˖u3פx7KvG9H֠cUMMӄfn*YR"ɳsYӨnćy%'ܚ_ D- k͞g)Yp@M2u 6MeQRƮNIJV>h{@JNLh.,ΗS#mxыT=NAy,Ho Gd$X5Ioe,Hler@HTʅmxȆXGHIgy$T|up܍cˬ+{ xh?E"_}g@X00 Du8bz:)R)aV)nAgf?*.ᣆ h j2B>)tH_Z܀`\['XbË\}Rm"gy#r0ɽ X)*ie#E)jÑb1EV+E<+FC!pY2>ɋ2^PFT:,6 'lq4}yʫUqc /t7(u;*({hfxc2 4GQՂ`M)E6[mHQ QkEllN6Ȱ,qCNMs6)<)qjV(bKV*,G54wg$lp7OQ7g*c-0$>8gw\z%} T< {$ŢSo͓>KvOs?$s:"ew-F")h>y#U-۸Z,+i*}:YJ64fL”S "(71*&4PEm0KUL| aDbYqԛbwŀhH#:>ׁQBcz@..=G0")1cY(ްH+Ek&a|9ŀeS*~AGU!bG*XM.C gb9Ra!ZA7_l~A{v_movX{:y:u:kѸzNxPvf7ɸ{8!w {{JIwJInp]σpGx:hb~tB?|/reÿL {EhڍhڔnfOobB[i{d{bx{0ݾx\}S/l(^MFzNhT_3Tj =?%W (>&U !UAL/1M*W3bk&H-WyE/Vs7^åP?6[a|X:1J|S -8!{n$[|(3U`}{"RU@,K:s)h roԟ#)0JD1/_Tnyw;YΪ|@n,&#|Ҫ:(zllFm5t>Mv*Z+ah_ښ0_2xqqh%>%jKB]@sܭ!(HGYbz ךTc0or[⸖B>Xў=_*\q-ʸ/}Y(8Ag0#측m|_rν;({251xTECSl4l1Q|K^^Drtmj+µ+>Ee32l+sSo!b q4PekҴ![Nj{|2-+ yb/ 'I'zx,0)Yna.&rAZlL͟4j oK`Q(5L`:p)^P/E^kpHSDnvVft"ΎFJ6>g!W6~r&r[ &'˃J;sfD0!7D֬vѨc+7hoji_v`%qj0!Y89zp9qf׬JПTϽ|%xIU%Xs{<'# h&>qgyOߘbE$:T K[S4)jm eS:iKU*2hSd*fo\I]|z6RD@t 9]$3aQv-K.S-MĤE{E"FMT ǥ-zd8ko)|q|fɲiUA٩&p@d Gzf7u$yvNݲi C!G X+}ҁ O{bJleN)0 dc]Ľ,EL BlxYWBx72cbkyKRtf. P,(Щ.%ѫ喖W`/|WCXϓFƌ LyiLЮ5O!i!+EZ9fb?HsgN*Pɫ'/Av-JC1eELf@:|f~FgPnaJZy)(K") M'$T&7yWpf2"$\w `y*E q҇ywP SI(m>̬'IIIwUe, y̜̤ )( ) a5U,([s]®43uQy~r?{ eɳ,7??1%q<oa-^8knIԯᨭzhp6+*/M!rf,6iG,Dcg8mDeN3dxgm힛%no$a\0g̈́p.3iAoJ ԗF΄JʬLaۡ+Svc=@yHLdWklSf.8ypME cs.j%_VALݞܔ| =2 "xs|&Kgۆ`aՔ"$ (5OJ@|xGBWE2V5}vZUp.r\0ytꐞ@9,nSܺ#RT%.ׂR@<#B 9 /x2kk2%=B"kZcg y6#H'&d,V!V7:̴ zBsm[X‘8YRQ])mʅ"Rr}8 dtknB:>j\]j*Zz[4鉾p] <ţנSEە℻ɑ*'1i|\XO;>ȶ5 %V,^Pfc|GP6f,%Аo0uᅭ;LIK`8B#8c{z0`o&#dՌڙd ,%$ EOx~LO;r%_l7y +Nbf᜙%3qpȳP$ .ucrcqǔKUYq 0XX9ӆHiJ6 H/:䰔4'͓o ]tZ\ӼbEOYl6Q yi@BO;{'-钀gZw%fn@)GO( {5jR++aeL+U|Rnl)_o%*~<pquj`&1}!$]nx;/zeK=:Ng4>,Ik3Bգ:(xq1 ͓ A*:5E(QBb Haz2vܞR'Lx&{u/H!nAA1KghftMT3ݬ,jyo$"穀 L׊i9BʪGʊ hcPccy07D0U#b7z@p~CU=2FH@WI|+h(E16L\7’.i.U K,TZ9P'ϖ0n㬥Cq&B9ʺPeXT؃pǿng'3:lA6D }k}VLZGykYw UO 9=87l 3Ӓ%A3_òH/,g,Do/᫇YL82q.171[كwbv|[? y/'0{d%xaBJ#&b}E6̭׆]1H@r®1Ytr,:7 Dq+Kv.DYp,C'E\1  %fK6,'HNZvbRfVHiaI%\;g ͆TRJ>\<>?\9b˚kw]6$qmO q,ζ H4qt z|gqxr⏜"~H eO MYS,IU80U QtSr^HL?r wnlLZ l-ƴW &Kw`eNvDa'Qt?mM#SK[_]* p`Cgh0`|W7(ԝҲgA̡Lu9fTefÒ %uq_ſrVJND= s/ePOP>6|޲!2/7SF`nۊY fA&Kֳu[ u,TLiyyQ0 \E8ջ~np~Hf1ٿa.#K ܖs(0O71Zl i[M4WCG\@8W ?N'DKfmC`86Y {2eN][ @84 ;)Qbp`%M*bLsuxs !kHWz܀Vr.#"{s&eM`aY*Jtxx7%!q}H '-mX; BL1[FK9V!b|3LCmhXbG \(j_$u(klOZAttM}߈.ʈOReݒ9MHjb̈°S# 湓,dw.9L("12>^ OŐ_&hxOF9ldlƦG߃uPlWT}/땼9C`"b,CIje1#V.tG)ɀC:nP9xL~8pCf1sSޱEZ~e+K3cOpdJ2C{ª,I/jjYGAZi)|81 q]u_8D8giF|"q^^KuvOsNY'@7Qoa%&Kg+}>[[2mk*E ʰ.\"1ڍ?xj!9&Bɞٳmn}!i~>:~̶Ss{u>+~+IB4A*vz+Z;d}p^оm6cDhk%?h6f:Orh-mw䟤MrOgmq/og:97.mxZGi-3W3lʨieZvFR v*PKWyڌx`MڶH"hkVI%![n'MYYI~ Y^'{P/xmafc_W-4md@NWJ5V{dŠmbݞk9ʛ4k<낏smݺO2IFngLУ맬5BwX:O(̭޻́n#ڊb`1e^ u!WBgߺ\qWwoƏkBd|AijӃ<.%4]ŕn%gʚڒmhƏhghNs nq3yglRO)N1phVIL'?bB"Snͷ[aOt#|6!WBuӉކӅ Ux?E]v_pnަY|_nT+Q{0tԿGq~MTcakBYbBKyI>M cFl&Ld'ݤ0ͻfwkBr!''Ity^eY^)H:$?&?(NhNpci~F-"G04ߣ+S_H~Hw,e5$kJlsn燎-)ҝQWHoO$@*0Mn(p'¬Hu,Ep%>`K.TE ]`?~0WY}u1'Xtiܓvbzr4Ť`#S5ea+*bu Z~5b!cxmd-F18&lѤN$]u(NBjNw; Cq"ZDR:AޔG#ۓ BL5uT\!w_~ kEG<8ACb A:Ld3?"»]O_ǁOʢ>2j#oމ?6 NV\7l،jd}MW;m4ͿnD}M j;#ej\~ <$U8jʄ/Q Hi?FU3ҙ_bU"*OIȥdv{/eAl-tEHڻ쨬CMcbM Dǧn ه.mFo;$}qU é#C3c&0_ Q$ , ٦YUufhNF{>#r_Xr72lH G\q7*sN8tUEضp[}X1K3!O\ܺ^^@ g$Yu!ЏvPt)Ȭ4T2+3b? ug͡utl3r>z{@U=ԕ;b읃sm8'm۶mm։'Ϋzj{uZ^JI5ե ;eYC!51 Ms=iPR{Q&-X~>z5ȖƩ!5M5Wہچ{+q&$&M]WNRmN{0uێ"4P@<os7{aӓ0 ʮqH?Сg܆R Ay{vӌa$Fݯ3xuSvdV)[A벭eR1Uk^Pms!l:W qJY r6̙OvajVztt׬-2³]#]ŧx$A|{&eρ칀f4^+d'~4&#E~ΑH54U v s K{hО !V[C=HPq/ۚo"|}p$'PGaGO29.=xQ _=v&Cw|-*]TuEDdhjSFFTߙ=ɇd)剦h%hې47$l$藑RO:QC1h9`T:S/ʷcQTQcB']#xh ˧2QQV~t,EHl ݬ \>AZⰱ ;7hZ_;ML)mDd}**q%b^>Ȝо҂.#{7큕өޢ<0n&HBna{廇t,eRv_ åB¥EKKH"4u<ڐR& JIЈ;Jߊ,B#E>qE>5~G>!E>1E>iHLFG-G=aK~\mMbaau躛uarGrWDSL"1J4WD8GV@@N8rBt Iχ?qo22hYd&uɊuh\o&M'f,q.[V3nMvi-:_K'zI+;ș I>nU+)pW+m汎sN*5$ks]B`^Ҷx< 7nv0L$%B\L#&z<&b%3]<6ReK^ڪ97GIЁrwHm58ӣ\-"\sh#j9%=/%YK= +E8zZF ;* d&k+] b4E2ac3cYbb=0Xm1/VaC7/Ԟ{>^& $PqRש^MҘ*dҳay1q:eb{{"{l6dq.bkK, 1rG*L*5Rv5-pRn,pw0Q1In-|hRѱD @c؊:uK ß0a3'x^@D-a3's'UrE04u!]u8|p|_4-C4i9x)=`a 9CR349Y7h*ĜLs,NaOcVbbhb<~GGGGGRVf3bRfW=G(^ZHYNaJ`bc ܄܄/' bab1bmk&}+RgF62bggUC0{&aK^:Mٿl+ k'lٹKYCBriT=+=X<uheq; Y``Fn[畄{6WW;?ʂ"OD  R.W..7S^kD`ia(AH̱|7@NNLYco8_GqvfJf޵$0/jγYQ&'S`:acEfG3dDJeLZ,sNlK9?B]~{@!1DY9s SK&-U<~' O#jkIS=gjw30DBj1k NC")H ' |+bg\=dCZä<ݫuXRIyI2=QCU(bsLv1>mrq]ğmͷkPCGL2+Ån-Hۚ*mj gm̴ְox{NhNG6^mgRo Fv$'gvCɆ$ > ݷimlj c`Qr7bq,٨jr!T:4h' g\kIQu؇!d3 o N<+8ќmk۠wAZ#I2n{[|OΖOI(FF*C!`,p̙^}N6R86qfQlfȎD.Ch M8ϵ͘{tV7skr]@f-PQĴ( 0+;@c}}3 H˗~Pk]-2Jn@q궵fs(AG;Z@O ueo<_1Kr_F %`U&wXT)HLR7BnvyOvX2: HJI ͖GaѦ Ԅ2Է6ҡFm6fz:Q6v bev0й]G*q,UXg*!`jƄDQ]J!YJwc^Cow 5'yܲ;CCh_.OحknGzZ|7mhQ{@.ݶҮ;͌m̋}z[⎱I<5xSkѵtD"vJ[1'Cc̺̠)ڀ Àcs f#ppa_ISi+%`"[Zr+3pFMpKAUQdh}ڃ h@F;C6|)1~a O!{zKbh)!?@-5ɤl3ff;FSt-w_Qgtdnѡbb}e4ZÖ;TÓfΠ OrR-u/2v%YOSL5>D@+kkB}C`km^ Ґ˅-þ-wړl぀A#_ =RuXkrbnBy{$Ŋ\D߉W{_Ę!?MZ 3>T: b2EIGٔ:(KtuaGHYeշ)}Lc-%Ӆ䞑$Yv-w2[A㘹=a#y5t 牫L Ǯ/k"Ő58OԵt`~I.\\3+KDMfalMB+$kʥI",ԫtXi,Ӿj*l aM+9Mr_jYkux9_Nhɿ*ݝIu&j!k5ilX9C9^O{uz3hղc ,y(6{o]W{4Ch(wF?픐ܑAZ;AtĶY>`dl _aZhS%M̤[nr|]ߺcG(憩YO>76~|VӻZUF,mY*6a*w/V*nx^A=g_@@Z:]pnDU4FU?}=f,w6(R^$3>~ۆEE;^8ͨGn?܌0ph5B 2)k#c*ouGwo(Oodqޘ[`t ³Ŕ?-GLWH ͪ yFUGL-Sh ?6/lse}V*hU1Pr#v.\.y]F&=ijC}YBƫ\V2\)יLLy.vUO5f=:LvrZoZYd"Z~NVOQu/<>p9AQ[/ Cz${(4)dљU q!v|r|squ{}%~m9~z'*Sj^eakầGeV:__slFpPڙ\7gdk?KE.|REk͖-"IWr2+)#RU- +۷-sĝbU@^gnWBWIGZO."K7 پJ̪p67X!1~('~re6 +v|j(G4BdIME"m>4 clccްF3JԘgyڻvMy@2t>K:.[Oy'ȴ@ʧG\%9n7 Y<̜O_ (2o,Yj0]5P[/{e_@x .9ag~htY1 !qQ<[0 pg|l(L偖&nrN4uQ/N8 .Q,\*?zNLR#<t*- ӎ>O? ^τuț``=owbOVG^L n{x>4W:?U=dn^CQbn,u#ejтuMR_D*#/2x [-H2%նz=/ͤ5).r->B*1~J̈́$@48_!ì**;wƁ6/Nz%6I-ѺeU!m<;U<88ίw|M.nYk!TBa!M4b5-^tGM+Ǝ8PPQ&6ҹXA*&zcW<8~`'z^NOE u#;:aJJS{w,2ok5/l $ cQ=yg')o/E4?]ňI`jH8J2؇[[[[&h !09WRKTda\/@Wf}^F_1 /3DOp='gd\EKޓ[  5{ȡ0NQ/Ar!Q' QY&Vg+}(kD#rpXI`[;%D&`x"Uv&Wu|5ߨ(#nvcAb]ޅO2wyxNSe3oɊb.vJ0Pwz_3{_@x+$X;Tq#T 卤Z)>2exiᘑq0/m{k䀖=w ع3uc@Rvkn;=Ps;u2PwlC0OӐ˓WPy{)WPqkxlœ;tyO s4MϑѸEgw`OK%(Y'ge݌izy5M9y**\yyՍ"͟ri; E^bMŲ -5cqrJN]޿.Vl.?n(C2E$d?]{#@@)"B# b(* r6valI;'9d~)UJ$=P1>UokEgdNL` ˃!#_ż&yHR,U׻DZ Z1H@k4dj5 t+d\n})P?_n_z-z`i7綖1mW$]2T]d` :P>bHmG0gjuyKzKWW7} dB@֩ЃfUkږ3W-]SAbb6f쥍!VqFta(KJ+YsscU9gfL=G{;f='Ɲ "}n뙄nc[T= !@Mܳ ];vG%IkRLsWb8)B@YP$,kHWz7'Р V턷HZ@8|O6􆊧2E<t0:pr8mIi:|_G%ȠΕf;9ǃ8_2.oMB%aI˞1`ŮL<" ,6XG Qs g7Q-VG;s. ]|P2Z+GeV KH /(+Ϛh͓{uchO4 j@\\Lv0yh43uWޞ^Gw$,Uq~'K/NŘ^С 8ub4aeCcEe֔DxP^ 2{)duM%""{x (" }t1&dfIKbZ saK(:\vW+u_:jv_xeo;jBeV|f)<P373Dm3qJ|p+Rnڎa^|o4$CqgaN_bRŧ@tjsO?7}ȝިT+w ( cKRrW'B>s[S]=E#g&dLj"/>L;G+kB}{R2 O="SC^$|cЯhK[ZGAv41DCT$X18,k'n(K΄QެG`sjB@.# *p$9oObZ㉮dg >M]}|[D'U7|ݿ|Gg$*2_{9T2PsЏfH8&j']tf3L:?6+JFJ*x>+|jzޜHF&W-:͍6.H``nZT% QhT¸˪ӐkuN7oKYlֈ+$.i}a>w v1z,% qd%ms%UgžG}*7YӦ-'$۟?<Բq18"pǑIk,@=RIemz,FF)dx#6$UuX-v6Pe k=kN8ՒHρ2jR-kEQ`͛}K]^Z3$ydͯ%ӵU;k΁'"g^{xRXwRU'g1)cYvo XJYS{ȕ6c-GH$^%iu~YՍhu[:w7В\4o(یբA撕 ŨR [Zh#wBSmIxks֠'Qc``5 U#'I ّxK_t꺳U8ͤ_ozq^s-)x rVݣ#%&vtXG( Q &v=;^߹=C~;2j B.1ifӤ;Xzvpѝ1*!"zfq!9l1Sw>eL\kPfİ$d!i{Gږ)Z*Sq! HYmL aT;bPj $_B*K71bIʼn^s]OI? l*9n6ْF( -""wq}4XS3v'U2D., sR 2{Gj6G ve?u8D d'PxKmO#! /K"U1K"EO*;`|Ƭ9'۽wXGFTim[GkxsEsYMazV[JMN~jtC*TJG&&r jUz8J?C]*|w'8voy({+Tĝ&Y 7?H-<3+@+-zto2TŐ"5k4ݵ6].I\4 ly,L+]9梸j=9Z=po'UYٝ?C:4ѱxS]x. @5hr^o HsJc1w%X9/5lki*ҧA3¨˹d}PV`]Z^(h/3ʁWΣ8cܤ#/JfL4t41Qt-2\Gb^"Ν$L ,,Q)DEfA7oNnpSD$>%OB3f\P KSTQfn:Nj*:_#آk(tI14TcWry `~!pLxg?nm;=W7N0E]z먅]{1X7X6% bZ+=&IPN œ6EFHm/u HFq8>z™Ai=7t)_^$2\nY/qg1%Ġff"I ~ɥS IlTƤJ#5控MjN)C)2..'lBoUC_F :vÑ׋0ګom\kV\@ʡ^&~I DT1pT/?Wm̈́t}!mD5tG?>m?;Qn'Eц  D>6Z87Rϗ.Tk/ĥnDkeD1 u/YB'mG01 Tdf]n8| AF ʞ9@YyPIY!P NU"f4&&{l.[J4\ B^m1"\5n׀y@+G}.!MRJ9*J-gȜPph'=PW#,uWhL+>4/2+mGJ;=pxu Ui6]=O74NT¶;$TD~s @(vŸT1Ҿ̨  dt3p!>eDz^YFrķ`<:E_JAYsVU@$͊X//Ǭ)jLx")iL7&4AxݡbU.D= A|Mk~<k>#52/I.EsM j-BrӦ6Pؠ*X~-S 0^;ϳS,!˝AU-/iIb,߿[j/iHPND6 AÐ! snl'EDe2-w"gP JÄxv+FI;!xΈ>Ke/X6ƜX/Za!gVb~Qpr_*ϣZr(X.f9.j!2¾Ő#H `P# h6*| ;U!!{v/`1A)H692 l+L:ԟ#?9 2?hNiw*q9\.FAAdNk("nt 4Wf>j~~ſ.(&D^El>PI)J|ہ"8НO,R¼ĥ3FW.uupRgD#WQ U_pj}y IFALU'NrkyT)d9_pKC#yxd,?y?Iޘ9v㲼Uf[& AZVhvϪ\NbADP!,3r߸؅$o+R?ɨ\lF_ G |,}cYk!!OHQo+1.*;BGΟahO -g#n{{St~{"O؀|VZ?`'13MJRg!i|$bfM8Za%D{Mh$)3");Dw0uռ Qا2E7K0c's~ibFyk7M4&9C/z#[{ʢuyB5az8kDoQ+tcJ#Ry TR0^SM4rH,Dj'vk*hrn$vMӌ׉EskOH$'x7IR ' fp^/cѰ|g0 1_^W[}>D(<#ƈ`QI5K#tۘUx}{X=|c9CX^^K[tpID<8[6t]?\5̶2SO;Uw;_Jlĭ''~R;0L >%]Y@So_n I_C?"5SӶ2/鶼S4R:EδYI4z: Zm)"b8Uɵ9wc&54٤)?mEƌnwδo37 GWK[y*NLVN ijD# -: ,I\mmu첫M.Ә/o;k.}VWS}x U ȿFH,Nޣ3 ^C*xDH4١ΏZFZJv=$Uh 5Ej! $N1:v0#%&u;Vʀ$㨡 l <#7l(~IiHfњ`, .lWf ..ڔAZDs=89T -\@Ulb#QIeI٠WESaJeY=a.XpNZ[NJ+ʬl׼8ZKl.䶶)fU RayV3 ¢] /t 5Ѣ>,|*WaJ՛'@<(/f8ǵ,OVVpDTppŏgt#k Zgb5X9ed ==~5CŲeK5I~D<02$\\;ҋ2YPVxXH G9D^Rʭk?nmSP):XBO 9cPe18,AIa"" ެuTɼjehո!B *H賔lRYpղ:e): ` aNƲ[ _zPLyDXHN>ɽǎ{落k>RRsACv'v Ȱ_ 'd19o(hheDLHE:|EdAEXTЖؤ1qb:G`#FS +J*vi{0p=&ƚ_&QV1RI4kj**J m57E_]Jxy p8¬E_@aV"Vp*fr4 &¶f]J 梙k)0sZ >H7SQc Ek#ݢ a%\oE~-o:F[R[J VD@FAQZ3ao+>gFKi(V 7[}}!k<ESi1Ѿ1T|Z zteMg}w'p͞oPgo*MNŭWWiO%)դG$P|N>* !Q蠈Rʠ둁9-Ҁ^ϘFsqg`vAEi)nb8xב( ciBBu{ۜ%J,\ݶk(X][koЄ݄ ]mZ%h\TU8'KEmY.+Q:!E͈驄l_RݬPyӟ88sm_x|d!4As{HI4G'(__pۭqf J [~\8 Am f-7FT~[(;$ˏ=eRR;i dSgXt}LpN`6'_;%{qNݏGgya1fS*KYtWY碲R_.}'a'&<`{3CI}¸YF!'Myfߚ߭n=.>owO<ݖ:DD: _ fp7ICbUzyR-Qޥ0ps|* )!N1;Uᆪ6<0SP.[ndܡz?H"CiN,3b*$ҩW-Tzȃ\pCn%FV kި̸4ay8]ZFG  |lx-%{/][M]_;ܫ{ZQVL5-[INM[vAkEprD[I!vzSp>$faPSTD0s,B%!xV%5jN3E!:rW]۷ O%gy>L+e+圈M߉z_l 'wC>V"WV8 AH"]G-wbQYa.[{qU*8pDG>Z5 2?t|OT/T"J.~lo5g)C:Kz1ΈQmqH}SngՅ ݣVY;8x0>> up_){˦+x$HIM@l_9O/?U-]!tJ`AlNeRy}5:PYˆ"uX\$ 9ki5G! ü1鏙|n FjsoG>H,ұgAyX#wKg]eu z#uxI#_AWl-"LDU)ɹBQ[sg9[o\;6%u[P3Qhxa&mKF˳Z~Iw*h!3{eWLn"~$Pt"x!+EX dpn8obvXuV65Β:IV>_9Q৭BOF4ƐԛX<,ҽDžd͘dFC@ubR__y9ߜ9yF.uv&GQ֙ enDj[6 J?-ttITԾK yy7@k1:LOC^o^:1s'3_B×9"6c/86E[WDu7DlĿ%c{U&**9,Zm3W,l*_;|*Iя~h%gB-9ɓSsڒ{sKNki7_qSMjHg:羃v̨w"rgL8.}.y L6ܷHStwOTԓbjnr\BDtÄlU`Z՝[8pcJ{Q ߪ S?0X x\uQн;ܒ nj-V : JZ,@Oe?=jUD=ut"gk#Kt,;+jY<}W@`(4I^y)x+V˛TP;j21l+>c8Nz}Hu?;ש,~ [Kn E)ܞVS|*6s&#B]ef:wz|p 9xq)%"rD=av׾\|7ƍmG>!SHzqqD ˆE Wa t\(g?gK6^f[`:[RQi@Nk8auuɧ6qyC}6!1}*$d |>>m#_q8Ż/2ad--g{-,C}לc9Waf.HR>deIjZ3E ^F_ m[ݽ@ĀqGZ.m7w)qFGeāloT%(=ǡk'>]LR==eYb"lV){JS!Y30}*_;')I+9Zz'vI0\K5~we0lw`]h{ r}Nw@1}:n{9,al*oU GrUWBn 渚[fcn{?ꓳ(CN'3Oe]|qXh3o̡G g篋8ۖy9?~~)o#a1$c\QcTKdIlvE3>g;# M3rtJSeŒxOw2Ɏ\?i?u,Jp^xx&/۽gvpf@m-A #Gi{τ_&*=J%.`|t:Qb`dA5#r@Rbc\yId 6/{RS?|$J[[BG~('Kw_4"%V]y5*]t,Z >\ /KrP͊6.)cU8/qБ;1$v5";;!FccëǥppKf/6A},=&q&Ul"YiX/MYF}D+8i:3ٜm$է*KQn*G~Qb$V%OlOЅ}AL-2V2=bVgJ7KgG&dXe,gzflsǮzUJU|T`1ӛ 3QWPDꨥ:"+[8dzJ9k&Q*ڏ}Kv./'Ga3++j>>R#RS'_'ջ6i.^VN5ȶ=SIHz-ڡ'}?*9e7h܍%E-3>dp${M֧^o6jo= iuv )E|2ْ["o;W祤|"vCK ʧRϦlN-o@z wwd͐?9^D,}Yzg;c U/Y-W8#u$>fr"3m%,ʑm&ӕK _: )1EP&RVH,ðP-ðH.C|ic3rÇḴ>·踸+rj\(~5ڽׁG%?p J>5c"4Ӕ~.{g\2P%S4 `\StϘDZgDls-b;T1g48Z үHt.I%9rdx,-]!-v'/{'4v=]Z3< |IS"%yw͈dkf &Xnj(to!lYtR->V1z/l-Vnc$Ηܰp^6Z Oog16Sh=kg:rv\XM[.PKZCwe&RTqVDLjU@ :͖Ws_5]ŠƱYu#jS>mBto)D R |DMpwZRWu04VCm}J]vOO9#."‘!ޚDugʙ7pOo6psO(leLgy9NwǗF?d2(jVuٱFK'v#AMy>)cZLպG0 4#3D.$:t {҉vda""p]bu\<_IRR[pa9N]a|a ]TL .As=OO3m/ʷ Ut«_S H')V-'qKXKDA ~+JD8i_Ыn[u<[ϯmIk uѷ2U,{#φ_bkFA5` r6a6SʌuA\۠GZtbgVFe5'o%OcC{ G|u\0q/VESA5Vd뜁XjOxɦ)>& -ɳH#h0vBS8a"wʿm$P4pu0yk+m&ɜtg$K|!*-LfZ+U ݯ}TAgX+/!u%Ҵ\ݽzaw } mT yx:x\}4WQ +R _# C ɐs9Q<ŲЃK7C;I^-%*:K*%>oFqâX:Ur:{ϞyrAg$QQ )krtʂiMyrT8m|-6Lb)+HJAD"Ys; Z>۫ <:Zn*/x!6ޭ(rb]:<&_fTGr}'}E֜UӜlW'gF]No/;}lJ!@bw;҉Wtևɣ@nSk G*?+#9{v_EǕF&FQyh^(x3Sc[dO"WW~R=wv+QSar/0EG퉺CyYv8M'>xFi.uw)/_ VoЪui :"YV YYĦ7wuu0ʼo rz `ʌ|R6}-VV9[wp} Ahk 3ge$g!ua*wjlޒ}-oDd_~͔oIu(&v-jwe&jO9`[bj tᐲY/Ya7I7fkzcmg`F!mVҧ9j8F^z|E]s_+^Ry*V.Yy[AM~g>뗻<yV(ɘ14TZ2hms=:dcrOP"U&+22izAO;ٱu)|pt}?q;rh=][‰j~pVEJ˴|N$=4&z_׉G7׺chlc>zK:U(wǙY'Z3jT:9Z]g6=ɧyA $nvɡ[u4:!2я'>^ˁ?)sf1 gO:ccB>﯒w4#?7PΫk?HW&\ɖy^#OJRM3Sc҃'Z&e1cZx܆XWߵq=Y~b2zUEe>ނo*ZD* :N*&>WՠMom*!7 o%7JZ 3J^=qF>J62m6E9LFy\!(2H ."_m|TɭzZ#Zjtg qc(.DY^?[#[[HwśKK5r޿7z"vqٞ͘o KE À/-9AcxC677qKצRJ4pdNJ_OִZZuJM`#fV.e/K;r4[l2yXO ߖ<:  N;\!1nAl)DMeyg`hv74aړ9aP¿FY1JhbgSQʗ"]z',=YELk5!tbCw;! kM3~a2~ xŭ?4 -M#W9[n/ɖ7O )jmEq_IJ"JvG ( {5h቗õ˒^dj= n1Tnk K8psU;;k6r~H}wK=H{&e)JS^"òzj𲲟>yNasE9fH5\fj*]@'::{uw~eV $ iAfa65͉b-DRoFZDZ1 ם0;՜2Iq$2UfSmFW)&%sgۏzŬYp8Ee?e+6z1nkP{nj fGֈVd.Jf-`ZN幄UEɪ ՚/6IlȚSķj+הy񖵨Wyq~lj=1bz`~{~G<;o$C04q`h%9#2 nD;B΢◎>X҂l>'6Vr3?J=]1w^\?R'?I(JòE}oM))ֲ$[4_X}2K Dt&4 `d=f-w;j|`cq. wH{Ei DOo=qmƙ77Br,e mT,T~>=P'c9\}pQMȇ(ڭ=+3d/5zNvO:`n l?ڬG:=/OwJ|iw+!WPO5xڅҷMYX\ܥZYbrHsmCynEa]TA7IyR9Ew*q?RpHOH<>-nJI}!0P|#ZЈuD}H(cjݑEoUT{q]^Zv.Ch|hM[ v<"Y \|ftٿ$U,ZhN\[)=ќxw枆m 8G8Fz "Z(\2lqͅs"Z3,A q8$ @Is՜{! K"vk fo:w]"&㸝'%S'ouixH,_:ox(7hjr!3> u"QVPjQABIԌZ5u~]ҋU['DVg gC$ {e.t6wXpTv#3{|UNşK_,R?zWtl5sL#Gr 39Ј4;?.h_!;j\?wn%sլ oCIŗ- 4Jե0k٫\l;:ԱvM.d%GR23:w>QY m嬚m`M֔-,by`ukôWIV̅& &C*#c{>ѵV^^'9[țgDY?j[s7)oj*|"?Yqh#XPRjDIL]mcb'9?4y9r e\YmK5B菮 =Z ) lMEx7TN89h-I(޷5-Pco;9Stcm:Gppts)9UySܢO5g: my`H )b- &M/6T-Uw>MK8@ LZuV [,gx׵:?S!E6sii\s"y/кgHª؏P d{8P:OU|0]o~m mIGpGժAYĢ>e`iK!}A݀]L[ F)* 3<:UMlf>?^0ypD MA N~F Ş#T0hu' W4O&^fF8"Oh~u'\)؅ACWК  [otg 0!)?vp6 kLr5X?5? :;GOs<00pTPpOPor,,!u7D߁̝\-? XB~p% !~Msn#p=:&:, t,!,0@dF+)[WM E/8H71prB=M܀Ps  dϑ?u U[& 0}(d@kQ6<D\u5@hώ0 DkbtBZrjFI59 VίE̕(n! {ۃԚD@Ym%P p$ԆS%Md4?A^3EȐ~oJV+:2k"Rį`NeS{`wW3P:ȻO(G JJKHjTW$R L)kR?_h@k~xxY;!ruj#eha3Wdjo8dk>F>k6Zv}ulnC ֏cCˆ^|DLW: j*YjN Wwh0c%:4hK@Ss 4Ч<0u ~Q+o^R !Cڼ\'WU3[noẢ@AHH~OP6uvq¤7tF%GY!C_⡃~, "L+ٸ̀ R8O[p^c%rpP,E!mRnX"Il]%+ MS(lt`F#AGrff>MB6(B$tM_ qrurt7RVr)V68V"-8 &+Hh#f t'`ʥ`po#={_~=z; 2ij&^nhhئRn/18oɝ @ć? # }찗/@@];@N uVYHd?w>\lˁ2A.(0aݤQ 2MKwD U02K Q( &|L)x C|pSa ^RC7rs9@Jq}1 #2' Yqð tIPBUR^?G%UKK7h>= 00d׈h7+M'7ރ\&JI;Ҏ>:$ia`:4``(nݬN j@yv$M'W)Nw`!T :`L*W=d`ї΁>}=5qK0 5\bjl.i ~Q۔"4 F[%qN.: LAuRF$b[ ihNN@6sp(~$h@+(  0 w`rXJ\Bc09`ȅwe?o@+iL8;.WoucWaꅃ{w#:c! 2:Ȑ&FݯBĚ@h&%pF#Ԋa@^U Ndu34r {M(}K=4`~-ظ?aA6VaX=uئHEYTN7`$C@FS@׽BOYDH+cdK8j?a7e- +o&m0d+=WW"O~>)\nM#iKdM") zuB88vREblH%*N M<"ί4RZy׈KB/nɂ5!Iհ'/3&*qrrQ3CgJTKe܂Q%Lm?ԩY Ux,A/QuSzq,ɗCލH'h۬!; ZJ&μI2b ՜.^w}-) AFCjhA>6 <-$sMp+`ߔc4 .'M;+K_D3_G)kyh 濎`I$@} 0*Ile k'6dRh[1al4`~l4= m<'O9e[X&ӫ=磁ܰJ*U+DZ] `aKl[*tHE8~m[*N @$^^/A%٘5f=cGwAceK Ї Fo_&Y.1V)QK9#@ϷY.a{X,k Zv Ŏ }o)hLz K58zHVX4c7mL?<< n3]ZF?.3LjW"*Pxiz2= 3͸I)& LdB-U b(5i"͏8믩ӷ8 -u=kmcC8Ų嚂0ݎa!(.nbjdo- %,aR=݅/0(#r& JW.¯89?tݦ!:2\8 ?nXXX$uF ^E@A/_$pIAO28!@eD+?8J[)8Snqsw5_ Ey;uaB_ |V^LJΎaqcrt@G[|:D| :wg 5[A` rAx5 Pl̟=pA(2?Pv0yo#ؒ u-]s^2q&2+':%Z@& : :G"BjTu# 6n7il.ƞB#>$q@M^ 7B k\P#amLlht񥄁i4 ^ 5 pP"NvJ DCxB@ :- XV+¹S;'OH1 bH+5߯9|"dRJbZ0e !180Pl ƴgS\<xJ ^t ncO#2\O(D6i MB+v3.0xZ =:"VΉ99I*`Oa]2ѢQۡX1%`Dy&btoXwLVb,s=C`ϧKrˡ3jr= - 3N} *aFpvfKQb9(powtBS'`o6# )J+N:[2[ @G`AooY5 Tdi`_ؤ>\2Pb]-AG8 n=J@C^Ǣd7 ~SkE h58Ӡe{rgA3 &r $t;;ua͊#nk(ƀwe,䫅QsY؍A D޳ổ84!--l-`PK>5hPKxVdescription/desc_ko.txt=J@} n|.ݹNXb,m4L؂`Iq0Μy'P\;%2zd' /K3[ }2D e EȄV.*viyPͧ0_#Rb { xtѹ̲"JY&HB75BϩY]Npw~w>#.%>*pPK\PKxVregistry/H2Orestart_types.xcuSn0=|'zmHPjH%يZ$zjە*"U蔵Q禐z[zY$nnJk4j,A셺z elscQJZNkY9ڳW]u]'}<,!֘2OHa|GP,8xk;}hfl!,{X 'T LlA}ax<0oIΑ2]J\t&r]CҌ_vuku  c}+$bU}ߋ7]u<o? ,Uus1gL=)i*j<v PKlCPKxVlogger.propertiesн0݄w`a3EšAH ki{I c,9AL!mzБ"D8Jo4C]/w0: .:Tx@~]Ht8STn[ìƥB `5SO(b>mg!p~]{PK)PKxVregistry/H2Orestart_filters.xcuSN0vO1&t3&HB0zej3i u4ztNFk0V2Ab\(L#rh%A^Pڡ4H\2@U[ghm[ܞ}Pn's Cqӌ  u0+wTj4JUf=xi g}w?ק]uVR2BI  `e.tڨHkQF\O N:8|˜S֡xwRltq6n+u8m4eCbvPK[}C PKxVMETA-INF/manifest.xmlœ1O0g+"Nm!*3:K9[)R%ݻٞ:胶T(Xl-߳mm: `k= ?ϒct+)CM [,XMN6ȑ?S7;.&.$a9*9P-BO"D:B:r[`y9LOW}sz\YJ ! 1ZqR\"em/#^CLZgr0[loPe_PKfBPKxV#ǡ U#icon/H2Orestart.pngPKxV:\[ description.xmlPKxV~z"description/desc_en.txtPKxV>5h#H2Orestart.jarPKxV\<description/desc_ko.txtPKxVlC&>registry/H2Orestart_types.xcuPKxV)?logger.propertiesPKxVIU@registry/H2Orestart_filters.xcuPKxV[}C Bregistry/TypeDetection.xcuPKxVfBCMETA-INF/manifest.xmlPK AEH2Orestart-0.7.2/icon/000077500000000000000000000000001476273367000144645ustar00rootroot00000000000000H2Orestart-0.7.2/icon/H2Orestart.png000066400000000000000000000215251476273367000171740ustar00rootroot00000000000000PNG  IHDR\rfsRGBgAMA a pHYs\F\FCA"IDATx^xTEO! Dz ҤHP:Dz' ʋ""R( /A)* ME K)I$0xu=<{3 q0Z*eFCXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcXFcB5h4JNNӧ Q|aTpEF%'C9:$C4dj֬| wxzgiժUo }+~Сo_hQ{ x?hڣxQZ,#kS^}믿9gTT~,es0Wm˝;~N===Lu? !cUzGm^h:}w;%'o`#3Gyҷ~KCUʕ>S*_<X,]KjI}=TkJCa}f=,6"G ,`Νg qǯ tS8^koZ1zTD^yw/RdZXlwF9{ÛoI-Z5@); _|fbcKرQm4uj e"pj:͟ѰaCZr#]`!W0 Qy7S^BaQϓ'Ӫo-ŋ*,[vXհؔs 4ZY|= _EwZ.+k`'8AY Ii˚ =;"̙#ѷo_*SF2rݻ0,5kVZ|c HNK)mRJvt]a-cn>J,)߹a2YwcPJ6Ҿ=]7W[{1{nZpyb *TpwX\bK"8uaƻxrg`Lq=Q:vd(*Pw3g Anp.yჁE7|#kFOS.O8w.;n A߷;~ܺ sq)c$կ_?Yx@l3 *z8lmGTܗ,N=i1ES'O |>숊t#GҲed7XNYs Î/+CY4yOHRX ̝; $kt嫜ݕenԨHe @m *R$BR @B)wǎg'Pg0W .,B #ؓlϥ`GR @x8jڔ^~""W<Ƽ8PH!kd5jKJ|- wQq_V4R@X^ᇔw >žzBf7w2$'' k&a1JR#.hVc={ub3)WÆbA~~1W3/6R<˗'*i;v:<mڔn.  =~֭ȑ#`ݟ4i͚5KXAhh(=];}/"6ƍ'@T.1f-,A s~}D-]Ti~or-5B&OjMd}V޽-OCq[sa>_@T{q$(h6PH~3nݺycnyHQG~/`TabѣӺuwĩ_$uCUsko_ZԩS%YR߻wo|=Μ9KO?=Ϙx˰.E-ZڵME""rwŋhi Z8]pUUД)-1 !H=(FڵhĈ-+0Ο?ގh"PS+09^6Ə$9Bf Gw쾃;veγ" ^،4eٳn]Vr&{W wX[#CZ(y2u53Xǘ#wҳ֐5AYiiw``GzUS^ % ~=VE ,,LP[*4~|*Z4R ʗ/@0_wa`8LGm!ؗP!ZךʔM,LЂlX!49UTP%DǴB`7XVbQ_ iʔ4aBSܓ,L 44Z,C3f km(JE\\YzVbTR}UXu˚w-K&Ț58/ʚϜI!\z^v7o.k9#GpfBWt{NVYAS85mZ4)eFV0`ruڹ u~%&^scZl"w(Voߞz)w31@t钬aÆe9K,&Mڭ7o)x>/_NK.7޽{L…7jԈڶm+D()]}eVpxޜB {z7gիW)%%EM6ݻw˚sΟ?/_' *߿?i1~Li… iӦ9Zn0#,qqq,Yv<)EqkwW 68bccMǓRJo0by[1F:*por֣Gv"fz=g >q)q- 2?7믿6=bt#>>;J*eb<=}FUɼ?_dQgΜ֏Y^y-9yMK)[cŊ[쁡Y1 }ZA5O?#z+_uW8qB彩;V0DQrexZT=-P~ /Aw]wey?PTT Mx&N(xC׿LieV<TBY39s^Sx6mN:%?+WV*8w|1 JC%Jz&M$.\@7oɓ'LA s- ~xMfA͓O>)EvZf` ޞ8'( |Uba02~W4h ᱦr-BᙇŽ;z [o%kVzw=SUezk`Y ;p݄;%zw9sUC_(RSڏ|%x:M+5xoHHH5;wsVlRrM*U^>L;vL4_~Yn;[gl0WW(su$&unDN':2 vwq6,YwWg{XZ-܁ WǏ}3gӦM:vQL癕 3g鹞3FZY,>wyZ2GAC8|hѢ.w\OKvQTT9.v\<-* hsE$ |\E.d@ FoݶB 믿˖ݻw5SKj0zhYcZ` z[z˔Z@Ogg"u}T&"M iȑ,.82pc O%hp v('>1T28D^ G|vnt$H[Xa.Yw{[`z0h zZi'ˑf$$$xs/;#0 :c0b>fΊ #wvWྋtp'ҳs+ c I֮]K۷oxف95\a܃żUV^ڑQFF{!Ht\|@ ֧Oa \Z@V&))I(-|ӱWA?hCǽ@bbb -aUa<a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a/^IENDB`H2Orestart-0.7.2/logger.properties000066400000000000000000000007551476273367000171400ustar00rootroot00000000000000.level=ALL java.util.logging.ConsoleHandler.level=ALL java.util.logging.ConsoleHandler.formatter=HwpDoc.CustomLogFormatter java.util.logging.FileHandler.level=ALL java.util.logging.FileHandler.pattern=%h/.H2Orestart/import_%u.log java.util.logging.FileHandler.limit=4194304 java.util.logging.FileHandler.count=1 java.util.logging.FileHandler.formatter=HwpDoc.CustomLogFormatter java.util.logging.FileHandler.append=true handlers=java.util.logging.ConsoleHandler,java.util.logging.FileHandlerH2Orestart-0.7.2/package.properties000066400000000000000000000004721476273367000172500ustar00rootroot00000000000000#Written by the OOEclipseIntegration #Tue Jun 22 09:15:21 KST 2021 contents=description, description/desc_en.txt, description/desc_ko.txt, description.xml, icon, icon/H2Orestart.png, logger.properties, registry, registry/TypeDetection.xcu, registry/H2Orestart_filters.xcu, registry/H2Orestart_types.xcu, types.rdb H2Orestart-0.7.2/registry/000077500000000000000000000000001476273367000154045ustar00rootroot00000000000000H2Orestart-0.7.2/registry/H2Orestart_filters.xcu000066400000000000000000000015251476273367000216550ustar00rootroot00000000000000 5 Hwp2002_File com.sun.star.text.TextDocument ebandal.libreoffice.H2Orestart Hwp2002_Reader IMPORT ALIEN 3RDPARTYFILTER H2Orestart-0.7.2/registry/H2Orestart_types.xcu000066400000000000000000000015071476273367000213510ustar00rootroot00000000000000 0 ebandal.libreoffice.H2Orestart hwpx hwp false Hwp2002_Reader Hwp2002_File H2Orestart-0.7.2/registry/TypeDetection.xcu000066400000000000000000000010211476273367000206770ustar00rootroot00000000000000 ebandal.libreoffice.H2Orestart Hwp2002_File H2Orestart-0.7.2/source/000077500000000000000000000000001476273367000150345ustar00rootroot00000000000000H2Orestart-0.7.2/source/HwpDoc/000077500000000000000000000000001476273367000162205ustar00rootroot00000000000000H2Orestart-0.7.2/source/HwpDoc/CustomLogFormatter.java000066400000000000000000000041211476273367000226610ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc; import java.io.PrintWriter; import java.io.StringWriter; import java.text.SimpleDateFormat; import java.util.Date; import java.util.logging.Formatter; import java.util.logging.LogRecord; public class CustomLogFormatter extends Formatter { private static final SimpleDateFormat dateFormat = new SimpleDateFormat("MM-dd HH:mm"); private Date dat = new Date(); @Override public String format(LogRecord record) { StringBuffer buf = new StringBuffer(); dat.setTime(record.getMillis()); buf.append("[").append(dateFormat.format(dat)).append("] ") .append("(").append(record.getSourceClassName().substring(record.getSourceClassName().length()-12)+"."+record.getSourceMethodName().substring(0,4)).append(") ") .append(record.getLevel()).append(": ").append(formatMessage(record)); if (record.getThrown() != null) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); pw.println(); record.getThrown().printStackTrace(pw); pw.close(); buf.append(sw.toString()); } buf.append("\n"); return buf.toString(); } } H2Orestart-0.7.2/source/HwpDoc/ErrCode.java000066400000000000000000000030151476273367000204050ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc; public enum ErrCode { UNDEFINED (0), SIGANTURE_NOT_MATCH (1), INVALID_MAJORVERSION (2), INVALID_MINORVERSION (3), INVALID_BYTEORDER (4), INVALID_SECTORSHIFT (5), INVALID_MINISECTORSHIFT (6), INVALID_NUM_DIRECTORYSECTOR (7), INVALID_MINI_STREAM_CUTOFF (8), FILE_READ_ERROR (9), INVALID_ZIP_DATA_FORMAT (10), ; private int errCode; ErrCode(int code) { this.errCode = code; } public void set(int errCode) { this.errCode = errCode; } public int get() { return errCode; } } H2Orestart-0.7.2/source/HwpDoc/Exception/000077500000000000000000000000001476273367000201565ustar00rootroot00000000000000H2Orestart-0.7.2/source/HwpDoc/Exception/CompoundDetectException.java000066400000000000000000000027411476273367000256210ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.Exception; import HwpDoc.ErrCode; public class CompoundDetectException extends Exception { private static final long serialVersionUID = -583571405184537607L; private ErrCode errCode; public CompoundDetectException() { this(ErrCode.UNDEFINED); } public CompoundDetectException(ErrCode errCode) { super(); this.errCode = errCode; } public ErrCode getReason() { return errCode==null?ErrCode.UNDEFINED:errCode; } } H2Orestart-0.7.2/source/HwpDoc/Exception/CompoundParseException.java000066400000000000000000000025221476273367000254600ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.Exception; import HwpDoc.ErrCode; public class CompoundParseException extends Exception { private static final long serialVersionUID = -583571405184537607L; private ErrCode errCode; public CompoundParseException() { this(ErrCode.UNDEFINED); } public CompoundParseException(ErrCode errCode) { super(); this.errCode = errCode; } } H2Orestart-0.7.2/source/HwpDoc/Exception/HwpParseException.java000066400000000000000000000037671476273367000244460ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.Exception; import HwpDoc.ErrCode; public class HwpParseException extends Exception { private static final long serialVersionUID = -6388448371538804607L; private ErrCode errCode; public HwpParseException() { super(); } public HwpParseException(ErrCode errCode) { super(errCode.toString()); this.errCode = errCode; } public HwpParseException(ErrCode errCode, String messsage) { super(messsage); this.errCode = errCode; } public HwpParseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } public HwpParseException(String message, Throwable cause) { super(message, cause); } public HwpParseException(String message) { super(message); } public HwpParseException(Throwable cause) { super(cause); } public ErrCode getReason() { return errCode==null?ErrCode.UNDEFINED:errCode; } } H2Orestart-0.7.2/source/HwpDoc/Exception/NotImplementedException.java000066400000000000000000000023131476273367000256230ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.Exception; public class NotImplementedException extends Exception { private static final long serialVersionUID = -583571405184537607L; public NotImplementedException(String function) { super(function); } } H2Orestart-0.7.2/source/HwpDoc/Exception/OwpmlParseException.java000066400000000000000000000025111476273367000247700ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.Exception; import HwpDoc.ErrCode; public class OwpmlParseException extends Exception { private static final long serialVersionUID = -583571405184537607L; private ErrCode errCode; public OwpmlParseException() { this(ErrCode.UNDEFINED); } public OwpmlParseException(ErrCode errCode) { super(); this.errCode = errCode; } } H2Orestart-0.7.2/source/HwpDoc/HanType.java000066400000000000000000000002631476273367000204340ustar00rootroot00000000000000package HwpDoc; public enum HanType { NONE (0x0), HWP (0x1), HWPX (0x2); private int num; private HanType(int num) { this.num = num; } } H2Orestart-0.7.2/source/HwpDoc/HwpDetectException.java000066400000000000000000000037351476273367000226410ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc; public class HwpDetectException extends Exception { private static final long serialVersionUID = -6388448371538804607L; private ErrCode errCode; public HwpDetectException() { super(); } public HwpDetectException(ErrCode errCode) { super(errCode.toString()); this.errCode = errCode; } public HwpDetectException(ErrCode errCode, String messsage) { super(messsage); this.errCode = errCode; } public HwpDetectException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } public HwpDetectException(String message, Throwable cause) { super(message, cause); } public HwpDetectException(String message) { super(message); } public HwpDetectException(Throwable cause) { super(cause); } public ErrCode getReason() { return errCode==null?ErrCode.UNDEFINED:errCode; } } H2Orestart-0.7.2/source/HwpDoc/HwpDocInfo.java000066400000000000000000000364011476273367000210670ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; import HwpDoc.HwpElement.HwpRecord; import HwpDoc.HwpElement.HwpRecord_BinData; import HwpDoc.HwpElement.HwpRecord_BorderFill; import HwpDoc.HwpElement.HwpRecord_Bullet; import HwpDoc.HwpElement.HwpRecord_CharShape; import HwpDoc.HwpElement.HwpRecord_DocumentProperties; import HwpDoc.HwpElement.HwpRecord_FaceName; import HwpDoc.HwpElement.HwpRecord_IdMapping; import HwpDoc.HwpElement.HwpRecord_Numbering; import HwpDoc.HwpElement.HwpRecord_ParaShape; import HwpDoc.HwpElement.HwpRecord_Style; import HwpDoc.HwpElement.HwpRecord_TabDef; import HwpDoc.HwpElement.HwpTag; public class HwpDocInfo { private static final Logger log = Logger.getLogger(HwpDocInfo.class.getName()); public HanType hanType; private HwpxFile parentHwpx; private HwpFile parentHwp; public List recordList; public LinkedHashMap binDataList; public List faceNameList; public List borderFillList; public List charShapeList; public List numberingList; public List bulletList; public List paraShapeList; public List styleList; public List tabDefList; public CompatDoc compatibleDoc; public HwpDocInfo(HanType hanType) { recordList = new ArrayList(); binDataList = new LinkedHashMap(); faceNameList = new ArrayList(); borderFillList = new ArrayList(); charShapeList = new ArrayList(); numberingList = new ArrayList(); bulletList = new ArrayList(); paraShapeList = new ArrayList(); styleList = new ArrayList(); tabDefList = new ArrayList(); compatibleDoc = CompatDoc.HWP; this.hanType = hanType; } public HwpDocInfo(HwpxFile parent) { this(HanType.HWPX); this.parentHwpx = parent; } public HwpDocInfo(HwpFile parent) { this(HanType.HWP); this.parentHwp = parent; } boolean parse(byte[] buf, int version) throws HwpParseException { int off = 0; while(off < buf.length) { int header = buf[off+3]<<24&0xFF000000 | buf[off+2]<<16&0xFF0000 | buf[off+1]<<8&0xFF00 | buf[off]&0xFF; int tagNum = header&0x3FF; // 10 bits (0 - 9 bit) int level = (header&0xFFC00)>>>10; // 10 bits (10-19 bit) int size = (header&0xFFF00000)>>>20; // 12 bits (20-31 bit) if (size==0xFFF) { size = buf[off+7]<<24&0xFF000000 | buf[off+6]<<16&0xFF0000 | buf[off+5]<<8&0xFF00 | buf[off+4]&0xFF; off += 8; } else { off += 4; } HwpRecord record = null; HwpTag tag = HwpTag.from(tagNum); log.fine(IntStream.rangeClosed(0, level).mapToObj(i -> String.valueOf(i)).collect(Collectors.joining())+"[TAG]="+tag.toString()+" ("+size+")"); switch(tag) { case HWPTAG_DOCUMENT_PROPERTIES: record = new HwpRecord_DocumentProperties(this, tagNum, level, size, buf, off, version); recordList.add(record); break; case HWPTAG_ID_MAPPINGS: record = new HwpRecord_IdMapping(this, tagNum, level, size, buf, off, version); recordList.add(record); break; case HWPTAG_BIN_DATA: HwpRecord_BinData binRecord = new HwpRecord_BinData(this, tagNum, level, size, buf, off, version); binDataList.put(binRecord.itemId, binRecord); break; case HWPTAG_FACE_NAME: record = new HwpRecord_FaceName(this, tagNum, level, size, buf, off, version); faceNameList.add(record); break; case HWPTAG_BORDER_FILL: record = new HwpRecord_BorderFill(this, tagNum, level, size, buf, off, version); borderFillList.add(record); break; case HWPTAG_CHAR_SHAPE: record = new HwpRecord_CharShape(this, tagNum, level, size, buf, off, version); charShapeList.add(record); break; case HWPTAG_TAB_DEF: record = new HwpRecord_TabDef(this, tagNum, level, size, buf, off, version); tabDefList.add(record); break; case HWPTAG_NUMBERING: record = new HwpRecord_Numbering(this, tagNum, level, size, buf, off, version); numberingList.add(record); break; case HWPTAG_BULLET: record = new HwpRecord_Bullet(this, tagNum, level, size, buf, off, version); bulletList.add(record); break; case HWPTAG_PARA_SHAPE: record = new HwpRecord_ParaShape(this, tagNum, level, size, buf, off, version); paraShapeList.add(record); break; case HWPTAG_STYLE: record = new HwpRecord_Style(this, tagNum, level, size, buf, off, version); styleList.add(record); break; case HWPTAG_COMPATIBLE_DOCUMENT: compatibleDoc = CompatDoc.from(buf[off+3]<<24&0xFF000000 | buf[off+2]<<16&0x00FF0000 | buf[off+1]<<8&0x0000FF00 | buf[off]&0x000000FF); break; case HWPTAG_LAYOUT_COMPATIBILITY: break; case HWPTAG_DOC_DATA: case HWPTAG_DISTRIBUTE_DOC_DATA: case HWPTAG_TRACKCHANGE: case HWPTAG_MEMO_SHAPE: case HWPTAG_FORBIDDEN_CHAR: case HWPTAG_TRACK_CHANGE: case HWPTAG_TRACK_CHANGE_AUTHOR: break; default: } off += size; } return true; } boolean readContentHpf(Document document, int version) throws HwpParseException, NotImplementedException { Element element = document.getDocumentElement(); NodeList nodeList = element.getChildNodes(); for (int i = 0; i < nodeList.getLength(); i++) { Node node = nodeList.item(i); HwpRecord_BinData record = null; switch(node.getNodeName()) { case "opf:metadata": break; case "opf:manifest": { NodeList children = node.getChildNodes(); for (int j=0; j */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.HwpElement; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; public class HwpRecord { private static final Logger log = Logger.getLogger(HwpRecord.class.getName()); private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); HwpTag tag; int level; int size; HwpRecord(int tagNum, int level, int size) { this(HwpTag.from(tagNum), level, size); } HwpRecord(HwpTag tag, int level, int size) { this.tag = tag; this.level = level; this.size = size; } public static void dump(byte[] buf, int off, int size) { int offset = off; while(offset < off+size) { char[] hexChars = new char[16 * 2]; for (int j=0; j<16 && offset+j>> 4]; hexChars[j*2+1] = HEX_ARRAY[v & 0x0F]; } offset += 16; log.finer(new String(hexChars)); } } public static void dump(String str) { log.finer(str); } public static void dumpNode(Node node, int depth) { NamedNodeMap attributes = node.getAttributes(); if (attributes != null) { StringBuffer sb = new StringBuffer(); for (int i=0; i " ").collect(Collectors.joining()) + node.getNodeName()+"="+node.getNodeValue()+",[" + sb.toString() + "]"); } NodeList children = node.getChildNodes(); if (children != null) { for (int i=0; i>> 4]; hexChars[i*2+1] = HEX_ARRAY[v & 0x0F]; } return new String(hexChars); } } H2Orestart-0.7.2/source/HwpDoc/HwpElement/HwpRecordTypes.java000066400000000000000000000207411476273367000240610ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.HwpElement; public class HwpRecordTypes { // 밑줄 public static enum LineStyle1 { SOLID (0), // 실선 DASH (1), // 긴 점선 DOT (2), // 점선 DASH_DOT (3), // -.-.-.- DASH_DOT_DOT (4), // -..-..-..- LONG_DASH (5), // Dash 보다 긴 선분의 반복 CIRCLE (6), // Dot보다 큰 동그라미의 반복 DOUBLE_SLIM (7), // 2중선 SLIM_THICK (8), // 가는선+굵은선 2중선 THICK_SLIM (9), // 굵은선+가는선 2중선 SLIM_THICK_SLIM (10), // 가는선+굵은선+가는선 3중선 WAVE (11), // 물결 DOUBLE_WAVE (12), // 물결 2중선 THICK_3D (13), // 두꺼운 3D THICK_3D_REVERS_LI (14), // 두꺼운 3D(광원 반대) SOLID_3D (15), // 3D 단선 SOLID_3D_REVERS_LI (16); // 3D 단선(광원 반대) private int num; private LineStyle1(int num) { this.num = num; } public static LineStyle1 from(int num) { for (LineStyle1 shape: values()) { if (shape.num == num) return shape; } return SOLID; } } // 테두리선, 취소선, 단 구분선 public static enum LineStyle2 { NONE (0), SOLID (1), // 실선 DASH (2), // 긴 점선 DOT (3), // 점선 DASH_DOT (4), // -.-.-.- DASH_DOT_DOT (5), // -..-..-..- LONG_DASH (6), // Dash 보다 긴 선분의 반복 CIRCLE (7), // Dot보다 큰 동그라미의 반복 DOUBLE_SLIM (8), // 2중선 SLIM_THICK (9), // 가는선+굵은선 2중선 THICK_SLIM (10), // 굵은선+가는선 2중선 SLIM_THICK_SLIM (11); // 가는선+굵은선+가는선 3중선 private int num; private LineStyle2(int num) { this.num = num; } public static LineStyle2 from(int num) { for (LineStyle2 shape: values()) { if (shape.num == num) return shape; } return NONE; } } public static enum NumberShape1 { DIGIT (0), // 1, 2, 3 CIRCLE_DIGIT (1), // 동그라미 쳐진 1, 2, 3 ROMAN_CAPITAL (2), // I, II, III ROMAN_SMALL (3), // i, ii, iii LATIN_CAPITAL (4), // A, B, C LATIN_SMALL (5), // a, b, c CIRCLED_LATIN_CAPITAL (6), // 동그라미 쳐진 A, B, C CIRCLED_LATIN_SMALL (7), // 동그라미 쳐진 a, b, c HANGLE_SYLLABLE (8), // 가, 나, 다 CIRCLED_HANGUL_SYLLABLE (9), // 동그라미 쳐진 가, 나, 다 HANGUL_JAMO (10), // ㄱ, ㄴ, ㄷ CIRCLED_HANGUL_JAMO (11), // 동그라미 쳐진 ㄱ, ㄴ, ㄷ HANGUL_PHONETIC (12), // 일, 이 , 삼, IDEOGRAPH (13), // 一, 二, 三 CIRCLED_IDEOGRAPH (14); // 동그라미 쳐진 一, 二, 三 private int num; private NumberShape1(int num) { this.num = num; } public static NumberShape1 from(int num) { for (NumberShape1 shape: values()) { if (shape.num == num) return shape; } return DIGIT; } } public static enum NumberShape2 { DIGIT (0), // 1, 2, 3 CIRCLE_DIGIT (1), // 동그라미 쳐진 1, 2, 3 ROMAN_CAPITAL (2), // I, II, III ROMAN_SMALL (3), // i, ii, iii LATIN_CAPITAL (4), // A, B, C LATIN_SMALL (5), // a, b, c CIRCLED_LATIN_CAPITAL (6), // 동그라미 쳐진 A, B, C CIRCLED_LATIN_SMALL (7), // 동그라미 쳐진 a, b, c HANGLE_SYLLABLE (8), // 가, 나, 다 CIRCLED_HANGUL_SYLLABLE (9), // 동그라미 쳐진 가, 나, 다 HANGUL_JAMO (10), // ㄱ, ㄴ, ㄷ CIRCLED_HANGUL_JAMO (11), // 동그라미 쳐진 ㄱ, ㄴ, ㄷ HANGUL_PHONETIC (12), // 일, 이 , 삼, IDEOGRAPH (13), // 一, 二, 三 CIRCLED_IDEOGRAPH (14), // 동그라미 쳐진 一, 二, 三 DECAGON_CIRCLE (15), // 갑, 을, 병, 정, 무, 기, 경, 신, 임, 계 DECAGON_CRICLE_HANGJA (16), // 甲, 乙, 丙, 丁, 戊, 己, 庚, 辛, 壬, 癸 SYMBOL (0x80), // 4가지 문자가 차례로 반복 USER_CHAR (0x81); // 사용자 지정 문자 반복 private int num; private NumberShape2(int num) { this.num = num; } public static NumberShape2 from(int num) { for (NumberShape2 shape: values()) { if (shape.num == num) return shape; } return DIGIT; } } public static enum LineArrowStyle { NORMAL (0), // 모양없음 ARROW (1), // 화살모양 SPEAR (2), // 라인모양 CONCAVE_ARROW (3), // 오목한 화살모양 DIAMOND (4), // 속이 찬 다이아몬드 모양 CIRCLE (5), // 속이 찬 원 모양 BOX (6), // 속이 찬 사각모양 EMPTY_DIAMOND (7), // 속이 빈 다이아몬드 모양 EMPTY_CIRCLE (8), // 속이 빈 원 모양 EMPTY_BOX (9); // 속이 빈 사각모양 private int num; private LineArrowStyle(int num) { this.num = num; } public static LineArrowStyle from(int num, boolean fill) { switch(num) { case 0: return NORMAL; case 1: return ARROW; case 2: return SPEAR; case 3: return CONCAVE_ARROW; case 4: case 7: return fill?DIAMOND:EMPTY_DIAMOND; case 5: case 8: return fill?CIRCLE:EMPTY_CIRCLE; case 6: case 9: return fill?BOX:EMPTY_BOX; default: return NORMAL; } } } public static enum LineArrowSize { SMALL_SMALL (0), // 작은-작은 SMALL_MEDIUM (1), // 작은-중간 SMALL_LARGE (2), // 작은-큰 MEDIUM_SMALL (3), // 중간-작은 MEDIUM_MEDIUM (4), // 중간-중간 MEDIUM_LARGE (5), // 중간-큰 LARGE_SMALL (6), // 큰-작은 LARGE_MEDIUM (7), // 큰-중간 LARGE_LARGE (8); // 큰-큰 private int num; private LineArrowSize(int num) { this.num = num; } public static LineArrowSize from(int num) { for (LineArrowSize shape: values()) { if (shape.num == num) return shape; } return MEDIUM_MEDIUM; } } } H2Orestart-0.7.2/source/HwpDoc/HwpElement/HwpRecord_BinData.java000066400000000000000000000160161476273367000244160ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.HwpElement; import java.nio.charset.StandardCharsets; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import HwpDoc.HanType; import HwpDoc.HwpDocInfo; import HwpDoc.Exception.HwpParseException; public class HwpRecord_BinData extends HwpRecord { private static final Logger log = Logger.getLogger(HwpRecord_BinData.class.getName()); public Type type; public Compressed compressed; public State state; public String aPath; // Type이 "LINK"일때, 연결 파일의 절대 경로 // public String rPath; // Type이 "LINK"일때, 연결 파일의 상대 경로 public short binDataID; // Type이 "EMBEDDING"이거나 "STORAGE"일때, BINDATASTORAGE에 저장된 바이너리 데이터의 아이디 public String format; // Type이 "EMBEDDING"일때, extension("."제외) public String itemId; // hwpx에서는 itemId가 String HwpRecord_BinData(int tagNum, int level, int size) { super(tagNum, level, size); } public HwpRecord_BinData(HwpDocInfo docInfo, int tagNum, int level, int size, byte[] buf, int off, int version) throws HwpParseException { this(tagNum, level, size); if (docInfo.hanType == HanType.HWP) { if (docInfo.getParentHwp().getBinData() == null) { docInfo.getParentHwp().setBinData(docInfo.getParentHwp().getOleFile().getChildEntries("BinData")); } } int offset = off; short typeBits = (short) (buf[offset + 1] << 8 & 0xFF00 | buf[offset] & 0x00FF); offset += 2; type = Type.from(typeBits & 0x0F); compressed = Compressed.from(typeBits & 0x30); state = State.from(typeBits & 0x300); int pathLen1 = 0, pathLen2 = 0; if (type == Type.LINK) { pathLen1 = (buf[offset + 1] << 8 & 0xFF00 | buf[offset] & 0x00FF) * 2; offset += 2; if (pathLen1 > 0) { aPath = new String(buf, offset, pathLen1, StandardCharsets.UTF_16LE); offset += pathLen1; log.finest(" " + aPath + "(AbsoluteLink)"); } pathLen2 = (buf[offset + 1] << 8 & 0xFF00 | buf[offset] & 0x00FF) * 2; offset += 2; if (pathLen2 > 0) { // rPath = new String(buf, offset, pathLen2, StandardCharsets.UTF_16LE); offset += pathLen2; // log.fine(" " + rPath + "(RelativeLink)"); } } if (type == Type.EMBEDDING || type == Type.STORAGE) { binDataID = (short) (buf[offset + 1] << 8 & 0xFF00 | buf[offset] & 0x00FF); offset += 2; // aPath = // docInfo.getParentHwp().getBinData().get(binDataID-1).getDirectoryEntryName().trim(); itemId = String.valueOf(binDataID); } if (type == Type.EMBEDDING || type == Type.STORAGE) { int extLen = (buf[offset + 1] << 8 & 0xFF00 | buf[offset] & 0x00FF) * 2; offset += 2; if (extLen > 0) { format = new String(buf, offset, extLen, StandardCharsets.UTF_16LE); offset += extLen; } aPath = String.format("BIN%04X.%s", binDataID, format); log.fine(" " + "ID=" + binDataID + "(" + aPath + ")"); } if (offset - off - size != 0) { throw new HwpParseException(); } } public HwpRecord_BinData(Node node, int version) { super(HwpTag.HWPTAG_BIN_DATA, 0, 0); NamedNodeMap attributes = node.getAttributes(); itemId = attributes.getNamedItem("id").getNodeValue(); Node tempNode = attributes.getNamedItem("isEmbeded"); if (tempNode != null) { switch (tempNode.getNodeValue()) { case "0": type = Type.LINK; if (attributes.getNamedItem("sub-path") != null) { aPath = attributes.getNamedItem("sub-path").getNodeValue(); } if (aPath == null) { aPath = attributes.getNamedItem("href").getNodeValue(); } break; case "1": type = Type.EMBEDDING; aPath = attributes.getNamedItem("href").getNodeValue(); break; } } else { aPath = attributes.getNamedItem("href").getNodeValue(); } format = attributes.getNamedItem("media-type").getNodeValue(); if (format.matches("image/(.*)")) { format = format.replaceAll("image/(.*)", "$1"); } } public static enum Type { LINK (0), EMBEDDING (1), STORAGE (2); private int type; private Type(int type) { this.type = type; } public static Type from(int type) { for (Type typeNum : values()) { if (typeNum.type == type) return typeNum; } return null; } } public static enum Compressed { FOLLOW_STORAGE (0x00), COMPRESS (0x10), NO_COMPRESS (0x20); private int comp; private Compressed(int comp) { this.comp = comp; } public static Compressed from(int comp) { for (Compressed compNum : values()) { if (compNum.comp == comp) return compNum; } return null; } } public static enum State { NEVER_ACCESSED (0x000), FOUND_FILE_BY_ACCESS(0x100), ACCESS_FAILED (0x200), LINK_ACCESS_IGNORED (0x400); private int state; private State(int state) { this.state = state; } public static State from(int state) { for (State stateNum : values()) { if (stateNum.state == state) return stateNum; } return null; } } } H2Orestart-0.7.2/source/HwpDoc/HwpElement/HwpRecord_BorderFill.java000066400000000000000000000644261476273367000251500ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.HwpElement; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.HwpDocInfo; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; import HwpDoc.HwpElement.HwpRecordTypes.LineStyle2; public class HwpRecord_BorderFill extends HwpRecord { private static final Logger log = Logger.getLogger(HwpRecord_BorderFill.class.getName()); private HwpDocInfo parent; final float[] LINE_THICK = { 0.1f, 0.12f, 0.15f, 0.2f, 0.25f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 1.0f, 1.5f, 2.0f, 3.0f, 4.0f, 5.0f }; public boolean threeD; // 3D효과의 유무 public boolean shadow; // 그림자 효과의 유무 public byte slash; // Slash 대각선 모양(시계 방향으로 각각의 대각선 유무를 나타냄, 왼쪽부터 차례대로 "0","2","3","6","7") public byte backSlash; // BackSlash 대각선 모양(반시계 방향으로 각각의 대각선 유무를 나타냄) public byte crookedSlash; // Slash 대각선 꺾은선 (slash,backslash의 가운데 대각선이 꺽어진 대각선임) public byte crookedBackSlash; // BaskSlash 대각선 꺽선 public boolean counterSlash; // Slash 대각선 모양 180도 회전 여부 public boolean counterBackSlash; // Backslash 대각선 모양 180도 회전 여부 public boolean breakCellSeparateLine; // 중심선 유무 public Border left; public Border right; public Border top; public Border bottom; public Border diagonal; public Fill fill; HwpRecord_BorderFill(int tagNum, int level, int size) { super(tagNum, level, size); left = new Border(); right = new Border(); top = new Border(); bottom = new Border(); diagonal = new Border(); } public HwpRecord_BorderFill(HwpDocInfo docInfo, int tagNum, int level, int size, byte[] buf, int off, int version) throws HwpParseException { this(tagNum, level, size); this.parent = docInfo; int offset = off; short typeBits = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; threeD = (typeBits&0x01)==0x01?true:false; shadow = (typeBits&0x02)==0x02?true:false; slash = (byte) ((typeBits>>>2)&0x07); backSlash = (byte) ((typeBits>>>5)&0x07); crookedSlash = (byte) ((typeBits>>>8)&0x03); crookedBackSlash = (byte) ((typeBits>>>10)&0x01); counterSlash = (typeBits&0x800)==0x800?true:false; counterBackSlash = (typeBits&0x1000)==0x1000?true:false; breakCellSeparateLine = (typeBits&0x2000)==0x2000?true:false; // Hwp 문서 파일 구조 5.0 에는 4방향정보로 4byte, 4byte, 16byte 읽어내는 것으로 명시했으나, // 실제 hwp 문서를 파싱하면서 판단하기에 1byte,1byte,4byte 4회 반복이 맞는 것 같다고 보임. left.style = LineStyle2.from(buf[offset++]); left.width = buf[offset++]; left.color = buf[offset+3]<<24&0xFF000000 | buf[offset]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset+2]&0x000000FF; // 0x00rrggbb offset += 4; right.style = LineStyle2.from(buf[offset++]); right.width = buf[offset++]; right.color = buf[offset+3]<<24&0xFF000000 | buf[offset]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset+2]&0x000000FF; offset += 4; top.style = LineStyle2.from(buf[offset++]); top.width = buf[offset++]; top.color = buf[offset+3]<<24&0xFF000000 | buf[offset]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset+2]&0x000000FF; offset += 4; bottom.style = LineStyle2.from(buf[offset++]); bottom.width = buf[offset++]; bottom.color = buf[offset+3]<<24&0xFF000000 | buf[offset]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset+2]&0x000000FF; offset += 4; diagonal.style = LineStyle2.from(buf[offset++]); diagonal.width = buf[offset++]; diagonal.color = buf[offset+3]<<24&0xFF000000 | buf[offset]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset+2]&0x000000FF; offset += 4; fill = new Fill(buf, offset, size-(offset-off)); offset += fill.getSize(); log.fine(" " +"ID="+(parent.borderFillList.size()+1) +",3D="+(threeD?"Y":"N") +",그림자="+(shadow?"Y":"N") +",S="+slash +",BS="+backSlash +",S꺾="+crookedSlash +",BS꺾="+crookedBackSlash +",S회전="+(counterSlash?"Y":"N") +",BS회전="+(counterBackSlash?"Y":"N") +",중심선="+(breakCellSeparateLine?"Y":"N") +",4테선종류=("+left.style.toString()+","+right.style.toString()+","+top.style.toString()+","+bottom.style.toString()+")" +",4테선굵기=("+(LINE_THICK[left.width])+","+(LINE_THICK[right.width])+","+(LINE_THICK[top.width]) +","+(LINE_THICK[bottom.width])+")" +",4테선색=("+String.format("%06X", left.color)+","+String.format("%06X", right.color)+","+String.format("%06X", top.color)+","+String.format("%06X", bottom.color)+")" +",대각선="+diagonal.style.toString() +",대각선굵기="+(LINE_THICK[diagonal.width]) +",대각선색깔="+String.format("%06X", diagonal.color) +",단색채우기="+(fill.isColorFill()?"Y"+String.format("(%06X)", fill.faceColor):"N") +",Grad채우기="+(fill.isGradFill()?"Y("+fill.gradType.toString()+")":"N") +",Img채우기="+(fill.isImageFill()?"Y("+fill.mode.toString()+")":"N")); // 왜 1byte 차이가 나는지 알수 없으나, 실제 문서에서 발생하므로 허용하기로 함. if (offset-off-size!=0) { log.fine("[TAG]=" + tagNum + ", size=" + size + ", but currentSize=" + (offset-off)); dump(buf, off, size); } } public HwpRecord_BorderFill(HwpDocInfo docInfo, Node node, int version) throws NotImplementedException { super(HwpTag.HWPTAG_BORDER_FILL, 0, 0); this.parent = docInfo; NamedNodeMap attributes = node.getAttributes(); // id는 처리하지 않는다. List에 순차적으로 추가한다. // String id = attributes.getNamedItem("id").getNodeValue(); switch(attributes.getNamedItem("threeD").getNodeValue()) { case "0": threeD = false; break; case "1": threeD = true; break; } switch(attributes.getNamedItem("shadow").getNodeValue()) { case "0": shadow = false; break; case "1": shadow = true; break; } // centerLine 처리는 어떻게 할지 모름. // attributes.getNamedItem("centerLine").getNodeValue(); switch(attributes.getNamedItem("breakCellSeparateLine").getNodeValue()) { case "0": breakCellSeparateLine = false; break; case "1": breakCellSeparateLine = true; break; } // fillBrush가 없는 경우를 위해 default로 fill 생성 fill = new Fill(); fill.fillType = 0; NodeList nodeList = node.getChildNodes(); for (int i=0; i 0) { colors = new int[colorNum]; if (colorNum > 2) { // 색상이 바뀌는 곳의 위치. 4bytes * 색 수 offset += (4 * (colorNum-2)); } } for (int i=0; i0) { stepCenter = buf[offset++]; offset += (moreSize-1); } if (fillType>0) { alpha = buf[offset++]; } // 이미지fill 일 경우, alpha값이 한번 더 있다. if ((fillType&0x02)==0x02) { alpha = buf[offset++]; } // 문서상에는 없으나, "추가 채우기 속성 길이"가 큰 값일 경우 무시하도록 한다. // if (extraSize>0 && offset-off-size==extraSize) { // extraFill = new byte[extraSize]; // System.arraycopy(buf, offset, extraFill, 0, extraSize); // offset += extraSize; // } if (offset-off-size!=0) { log.fine("[Fill] size=" + size + ", but currentSize=" + (offset-off)); dump(buf, off, size); } this.size = offset-off; } public int getSize() { return this.size; } public boolean isColorFill() { return (fillType&0x01)==0x01; } public boolean isGradFill() { return (fillType&0x04)==0x04; } public boolean isImageFill() { return (fillType&0x02)==0x02; } } public static class Border { public LineStyle2 style; // 선 종류 public byte width; // 굵기 (0.1/0.12/0.15/0.2/0.25/0.3/0.4/0.5/0.6/0.7/1.0/1.5/2.0/3.0/4.0/5.0 mm) public int color; // 색상 0xRRGGBB 값으로 저장하자. } public static enum ImageFillType { TILE (0), // 바둑판식으로 - 모두 TILE_HORZ_TOP (1), // 바둑판식으로 - 가로/위 TILE_HORZ_BOTTOM (2), // 바둑판식으로 - 가로/아래 TILE_VERT_LEFT (3), // 바둑판식으로 - 세로/왼쪽 TILE_VERT_RIGHT (4), // 바둑판식으로 - 세로/오른쪽 TOTAL (5), // 크기에 맞추어 CENTER (6), // 가운데로 CENTER_TOP (7), // 가운데 위로 CENTER_BOTTOM (8), // 가운데 아래로 LEFT_CENTER (9), // 왼쪽 가운데로 LEFT_TOP (10), // 왼쪽 위로 LEFT_BOTTOM (11), // 왼쪽 아래로 RIGHT_CENTER (12), // 오른쪽 가운데로 RIGHT_TOP (13), // 오른쪽 위로 RIGHT_BOTTOM (14), // 오른쪽 아래로 ZOOM (15), // 확대 NONE (16); // NONE private int fill; private ImageFillType(int fill) { this.fill = fill; } public static ImageFillType from(int fill) { for (ImageFillType typeNum: values()) { if (typeNum.fill == fill) return typeNum; } return null; } } public static enum GradFillType { LINEAR (1), // 줄무니형 RADIAL (2), // 원형 CONICAL (3), // 원뿔형 SQUARE (4); // 사각형 private int fill; private GradFillType(int fill) { this.fill = fill; } public static GradFillType from(int gradation) { for (GradFillType typeNum: values()) { if (typeNum.fill == gradation) return typeNum; } return null; } } public static enum ColorFillPattern { NONE (-1), VERTICAL (0), // - - - HORIZONTAL (1), // ||||| BACK_SLASH (2), // \\\\\ SLASH (3), // ///// CROSS (4), // +++++ CROSS_DIAGONAL (5); // xxxxx private int fill; private ColorFillPattern(int fill) { this.fill = fill; } public static ColorFillPattern from(int fill) { for (ColorFillPattern typeNum: values()) { if (typeNum.fill == fill) return typeNum; } return null; } } } H2Orestart-0.7.2/source/HwpDoc/HwpElement/HwpRecord_Bullet.java000066400000000000000000000173661476273367000243540ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.HwpElement; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.HwpDocInfo; import HwpDoc.Exception.HwpParseException; import HwpDoc.HwpElement.HwpRecord_Numbering.Numbering; public class HwpRecord_Bullet extends HwpRecord { private static final Logger log = Logger.getLogger(HwpRecord_Bullet.class.getName()); private HwpDocInfo parent; public Numbering headerInfo; // 문단 머리의 정보 public char bulletChar; // 글머리표 문자 public int bulletImage; // 이미지 글머리표 여부 (글머리표:0, 이미지글머리표: ID) // 이미지 글머리 public byte bright; // 밝기 public byte contrast; // 대비 public byte imageEffect; // 효과 public String binItemRefID; // ID public char checkBulletChar; // 체크 글머리표 문자 HwpRecord_Bullet(int tagNum, int level, int size) { super(tagNum, level, size); } public HwpRecord_Bullet(HwpDocInfo docInfo, int tagNum, int level, int size, byte[] buf, int off, int version) throws HwpParseException { this(tagNum, level, size); this.parent = docInfo; this.headerInfo = new Numbering(); int offset = off; int typeBits = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; headerInfo.align = (byte) ((typeBits)&0x03); headerInfo.useInstWidth = (typeBits&0x40)==0x40?true:false; headerInfo.autoIndent = (typeBits&0x80)==0x80?true:false; headerInfo.textOffsetType = (byte) ((typeBits>>>4)&0x01); headerInfo.widthAdjust = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; headerInfo.textOffset = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; headerInfo.charShape = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; bulletChar = ByteBuffer.wrap(buf, offset, 2).order(ByteOrder.LITTLE_ENDIAN).getChar(); offset += 2; if (size-(offset-off) > 0) { bulletImage = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; } if (size-(offset-off) > 0) { bright = buf[offset++]; } if (size-(offset-off) > 0) { contrast = buf[offset++]; } if (size-(offset-off) > 0) { imageEffect = buf[offset++]; } if (size-(offset-off) > 0) { binItemRefID = String.valueOf(buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF); offset += 2; } // size가 23byte일 경우, 아래 2byte는 읽지 않도록 한다. 문서도 정확하지 않으니 이게 맞는 것인지는 알 수 없다. if (size-(offset-off) >0 ) { checkBulletChar = ByteBuffer.wrap(buf, offset, 2).order(ByteOrder.LITTLE_ENDIAN).getChar(); offset += 2; } log.fine(" " // +"ID="+(parent.bulletList.size()) +"문단머리정보속성="+String.format("0x%08X", headerInfo.align) +(headerInfo.charShape!=-1?",글자모양="+((HwpRecord_CharShape)(parent.charShapeList.get(headerInfo.charShape))).fontName[0]:"") +",글머리표문자="+String.format("%c", bulletChar)+"("+(short)bulletChar+")" +",글머리표="+(bulletImage==0?"글머리표":"이미지글머리표("+String.valueOf(bulletImage)+")") +(bulletImage==0?"":",이미지ID="+bulletImage) +",밝기="+bright +",대비="+contrast +(binItemRefID!=null?",BinData="+binItemRefID:"") +",체크글머리표문자="+String.format("%c", checkBulletChar) ); if (offset-off-size!=0) { log.finest("[TAG]=" + tag.toString() + ", size=" + size + ", but currentSize=" + (offset-off)); dump(buf, off, size); throw new HwpParseException(); } } public HwpRecord_Bullet(HwpDocInfo docInfo, Node node, int version) throws HwpParseException { super(HwpTag.HWPTAG_BULLET, 0, 0); this.parent = docInfo; NamedNodeMap attributes = node.getAttributes(); for (int i=0; i */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.HwpElement; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.HwpDocInfo; import HwpDoc.Exception.HwpParseException; import HwpDoc.HwpElement.HwpRecordTypes.LineStyle1; import HwpDoc.HwpElement.HwpRecordTypes.LineStyle2; public class HwpRecord_CharShape extends HwpRecord { private static final Logger log = Logger.getLogger(HwpRecord_CharShape.class.getName()); private HwpDocInfo parent; public String[] fontName = new String[Lang.MAX.num]; // 언어별 글꼴명(FaceID에서 유도) // f# public short[] ratio = new short[Lang.MAX.num]; // 언어별 장평, 50%~200% // r# public short[] spacing = new short[Lang.MAX.num]; // 언어별 자간, -50%~50% // s# public short[] relSize = new short[Lang.MAX.num]; // 언어별 상대 크기, 10%~250% // e# public short[] charOffset = new short[Lang.MAX.num]; // 언어별 글자 위치, -100%~100% // o# public int height; // 기준 크기, 0pt~4096pt // he // public int attribute; // 속성 public boolean italic; // 기울임 여부 // it public boolean bold; // 진하게 여부 // bo public Underline underline; // 밑줄 종류 // ut public LineStyle1 underlineShape; // 밑줄 모양 // us public int underlineColor; // 밑줄 색 public Outline outline; // 외곽선종류 // public Shadow shadow; // 그림자 종류 // public boolean emboss; // 양각 여부 // em? public boolean engrave; // 음각 여부 // en? public boolean superScript; // 위 첨자 여부 // su? public boolean subScript; // 아래 첨자 여부 // sb? public byte strikeOut; // 취소선 여부 public Accent symMark; // 강조점 종류 public boolean useFontSpace; // 글꼴에 어울리는 빈칸 사용 여부 // uf? public LineStyle2 strikeOutShape; // 취소선 모양 public boolean useKerning; // kerning여부 // uk? public byte shadowOffsetX; // 그림자 간격, -100%~100% public byte shadowOffsetY; // 그림자 간격, -100%~100% public int textColor; // 글자 색 // public int shadeColor; // 음영 색 public int shadowColor; // 그림자 색 public short borderFillIDRef; // 글자 테두리/배경 ID(CharShapeBorderFill ID) 참조 값 public int strikeOutColor; // 취소선 색 HwpRecord_CharShape(int tagNum, int level, int size) { super(tagNum, level, size); } public HwpRecord_CharShape(HwpDocInfo docInfo, int tagNum, int level, int size, byte[] buf, int off, int version) throws HwpParseException { this(tagNum, level, size); this.parent = docInfo; int offset = off; for (int i=0; i < Lang.MAX.num; i++) { short fontID = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); fontName[i] = ((HwpRecord_FaceName)parent.faceNameList.get(fontID)).faceName; offset += 2; } for (int i=0; i < Lang.MAX.num; i++) { ratio[i] = (short) (buf[offset++] & 0x00FF); } for (int i=0; i < Lang.MAX.num; i++) { spacing[i] = (byte) (buf[offset++] & 0x00FF); } for (int i=0; i < Lang.MAX.num; i++) { relSize[i] = (short) (buf[offset++] & 0x00FF); } for (int i=0; i < Lang.MAX.num; i++) { charOffset[i] = (byte) (buf[offset++] & 0x00FF); } height = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; int attrBits = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; // Attributes italic = (attrBits&0x01)==0x01?true:false; bold = (attrBits&0x02)==0x02?true:false; underline = Underline.from((attrBits>>>2)&0x03); underlineShape = LineStyle1.from((attrBits>>>4)&0x0F); outline = Outline.from((attrBits>>>8)&0x7); shadow = Shadow.from((attrBits>>11)&0x03); emboss = (attrBits&0x2000)==0x2000?true:false; engrave = (attrBits&0x4000)==0x4000?true:false; superScript = (attrBits&0x8000)==0x8000?true:false; subScript = (attrBits&0xF000)==0xF000?true:false; strikeOut = (byte) ((attrBits>>>18)&0x07); symMark = Accent.from((attrBits>>>21)&0x0F); useFontSpace = (attrBits&0x2000000)==0x2000000?true:false; strikeOutShape = LineStyle2.from((attrBits>>>26)&0x0F); useKerning = (attrBits&0x40000000)==0x40000000?true:false; // Attributes shadowOffsetX = buf[offset++]; shadowOffsetY = buf[offset++]; textColor = buf[offset+3]<<24&0xFF000000 | buf[offset]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset+2]&0x000000FF; offset += 4; underlineColor = buf[offset+3]<<24&0xFF000000 | buf[offset]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset+2]&0x000000FF; offset += 4; shadeColor = buf[offset+3]<<24&0xFF000000 | buf[offset]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset+2]&0x000000FF; offset += 4; shadowColor = buf[offset+3]<<24&0xFF000000 | buf[offset]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset+2]&0x000000FF; offset += 4; if (offset-off < size) { borderFillIDRef = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; } if (version > 5030 && offset-off < size) { strikeOutColor = buf[offset+3]<<24&0xFF000000 | buf[offset]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset+2]&0x000000FF; offset += 4; } log.fine(" " +"ID="+(parent.charShapeList.size()) +",폰트명[0]="+fontName[0] +",장평="+ratio[0]+"%" +",자간="+spacing[0]+"%" +",크기="+relSize[0]+"%" +",위치="+charOffset[0]+"%" +",크기="+height+"pt" +",기울임="+(italic?"Y":"N") +",진하게="+(bold?"Y":"N") +",밑줄="+(underline==null?"???":underline.toString()) +",외곽선="+(outline==null?"???":outline.toString()) +",그림자="+(shadow==null?"???":shadow.toString()) +",글자색="+String.format("%06X", textColor) +",음영색="+String.format("%06X", shadeColor) +",테두리ID="+borderFillIDRef +(borderFillIDRef>0?",테두리=("+(((HwpRecord_BorderFill) (parent.borderFillList.get(borderFillIDRef-1))).left.style):"") +(borderFillIDRef>0?","+(((HwpRecord_BorderFill) (parent.borderFillList.get(borderFillIDRef-1))).right.style):"") +(borderFillIDRef>0?","+(((HwpRecord_BorderFill) (parent.borderFillList.get(borderFillIDRef-1))).top.style):"") +(borderFillIDRef>0?","+(((HwpRecord_BorderFill) (parent.borderFillList.get(borderFillIDRef-1))).bottom.style):"")+")" ); if (offset-off-size != 0 && offset-off-size+1 != 0) { log.fine("[TAG]=" + tagNum + ", size=" + size + ", but currentSize=" + (offset-off)); dump(buf, off, size); // throw new HwpParseException(); } } public HwpRecord_CharShape(HwpDocInfo docInfo, Node node, int version) { super(HwpTag.HWPTAG_CHAR_SHAPE, 0, 0); this.parent = docInfo; NamedNodeMap attributes = node.getAttributes(); // 초기값. 속성이 없을 경우, null 방지 underline = Underline.NONE; underlineShape = LineStyle1.DASH; outline = Outline.NONE; shadow = Shadow.NONE; symMark = Accent.NONE; strikeOutShape = LineStyle2.NONE; // id값은 처리하지 않는다. List에 순차적으로 추가한다. // String id = attributes.getNamedItem("height").getNodeValue(); height = 1000; String numStr = attributes.getNamedItem("height").getNodeValue(); height = Integer.parseInt(numStr); textColor = 0x000000; numStr = attributes.getNamedItem("textColor").getNodeValue(); if (!numStr.equals("none")) { numStr = numStr.replaceAll("#", ""); textColor = (int) Long.parseLong(numStr, 16); // RGBColor (0xRRGGBB) 값으로 저장 } shadeColor = 0xFFFFFFFF; if (attributes.getNamedItem("shadeColor") != null) { numStr = attributes.getNamedItem("shadeColor").getNodeValue(); if (!numStr.equals("none")) { numStr = numStr.replaceAll("#", ""); shadeColor = (int) Long.parseLong(numStr, 16); // RGBColor (0xRRGGBB) 값으로 저장 } } useFontSpace = false; switch(attributes.getNamedItem("useFontSpace").getNodeValue()) { case "0": useFontSpace = false; break; case "1": useFontSpace = true; break; } useKerning = false; switch(attributes.getNamedItem("useKerning").getNodeValue()) { case "0": useKerning = false; break; case "1": useKerning = true; break; } switch(attributes.getNamedItem("symMark").getNodeValue()) { case "NONE": symMark = Accent.NONE; break; case "DOT_ABOVE": symMark = Accent.DOT; break; case "RING_ABOVE": symMark = Accent.RING; break; case "TILDE": symMark = Accent.TILDE; break; case "CARON": case "SIDE": case "COLON": case "GRAVE_ACCENT": case "ACUTE_ACCENT": case "CIRCUMFLEX": case "MACRON": case "HOOK_ABOVE": case "DOT_BELOW": default: symMark = Accent.NONE; } numStr = attributes.getNamedItem("borderFillIDRef").getNodeValue(); borderFillIDRef = (short)Integer.parseInt(numStr); NodeList nodeList = node.getChildNodes(); for (int i=0; i 나중에 DROP으로 바꾸자. CONTINUOUS (0x2); // 연속 private int num; private Shadow(int num) { this.num = num; } public static Shadow from(int num) { for (Shadow shadow: values()) { if (shadow.num == num) return shadow; } return NONE; } } public static enum Accent { NONE (0x0), // 없음 DOT (0x1), // 검정 동그라미 강조점 RING (0x2), // 속 빈 동그라미 강조점 CARON (0x3), // V TILDE (0x4), // ~ ARAEA (0x5), // ㆍ TWOARAEA (0x6); // : private int num; private Accent(int num) { this.num = num; } public static Accent from(int num) { for (Accent accent: values()) { if (accent.num == num) return accent; } return NONE; } } } H2Orestart-0.7.2/source/HwpDoc/HwpElement/HwpRecord_CtrlData.java000066400000000000000000000100651476273367000246100ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.HwpElement; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import HwpDoc.Exception.HwpParseException; import HwpDoc.paragraph.Ctrl; public class HwpRecord_CtrlData extends HwpRecord { private static final Logger log = Logger.getLogger(HwpRecord_CtrlData.class.getName()); public static List paramSets; HwpRecord_CtrlData(int tagNum, int level, int size) { super(tagNum, level, size); } public static int parseCtrl(Ctrl ctrl, int size, byte[] buf, int off, int version) throws HwpParseException { int offset = off; // 한컴문서의 내용만으로는 어떻게 해석해야 하는지 알수 없다. /* paramSets = new ArrayList(); while(offset < size) { ParameterSet paramSet = new ParameterSet(); paramSet.paramSetId = (short) (buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF); offset += 2; paramSet.nItems = (short) (buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF); offset += 2; if (paramSet.nItems > 0) { paramSet.items = new ArrayList(); for (int i=0; i< paramSet.nItems; i++) { ParameterItem item = new ParameterItem(); item.itemId = (short) (buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF); offset += 2; int itemType = buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 2; item.itemType = ParamItemType.from(itemType); } } } */ if (log.isLoggable(Level.FINE)) { log.fine(" ctrlID="+ctrl.ctrlId); } // ctrlId를 거꾸로 읽어 비교한다. switch(ctrl.ctrlId) { case "klc%": // FIELD_CLICKHERE case "dces": // 구역정의 case "mrof": // 양식개체 case "pngp": // 쪽 번호 위치 case "klh%": // hyperlink case "frx%": // FIELD_CROSSREF case "knu%": // FIELD_UNKNOWN case "etd%": // FIELD_DATE case "tdd%": // FIELD_DOCDATE case "tap%": // FIELD_PATH case "kmb%": // FIELD_BOOKMARK case "gmm%": // FIELD_MAILMERGE case "umf%": // FIELD_FORMULA case "mkob": // ??? if (log.isLoggable(Level.FINE)) { log.fine(ctrl.ctrlId+"("+size+")를 해석할 수 없음. Just skipping..."); } break; default: log.severe("Neither known ctrlID=" + ctrl.ctrlId+" nor implemented."); } return size; } public static class ParameterSet { short paramSetId; short nItems; List items; } public static class ParameterItem { short itemId; ParamItemType itemType; byte[] itemData; } public static enum ParamItemType { PIT_NULL (0x0), PIT_BSTR (0x1), PIT_I1 (0x2), PIT_I2 (0x3), PIT_I4 (0x4), PIT_I (0x5), PIT_UI1 (0x6), PIT_UI2 (0x7), PIT_UI4 (0x8), PIT_UI (0x9), PIT_SET (0x8000), PIT_ARRAY (0x8001), PIT_BINDATA (0x8002); private int num; private ParamItemType(int num) { this.num = num; } public static ParamItemType from(int num) { for (ParamItemType type: values()) { if (type.num == num) return type; } return null; } } } H2Orestart-0.7.2/source/HwpDoc/HwpElement/HwpRecord_CtrlHeader.java000066400000000000000000000140541476273367000251310ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.HwpElement; import java.nio.charset.StandardCharsets; import java.util.logging.Logger; import HwpDoc.Exception.HwpParseException; import HwpDoc.paragraph.Ctrl; import HwpDoc.paragraph.Ctrl_AutoNumber; import HwpDoc.paragraph.Ctrl_Click; import HwpDoc.paragraph.Ctrl_ColumnDef; import HwpDoc.paragraph.Ctrl_EqEdit; import HwpDoc.paragraph.Ctrl_Form; import HwpDoc.paragraph.Ctrl_GeneralShape; import HwpDoc.paragraph.Ctrl_HeadFoot; import HwpDoc.paragraph.Ctrl_NewNumber; import HwpDoc.paragraph.Ctrl_Note; import HwpDoc.paragraph.Ctrl_PageNumPos; import HwpDoc.paragraph.Ctrl_SectionDef; import HwpDoc.paragraph.Ctrl_Table; public class HwpRecord_CtrlHeader extends HwpRecord { private static final Logger log = Logger.getLogger(HwpRecord_CtrlHeader.class.getName()); // public String ctrlId; // 컨트롤 ID // public Ctrl ctrl; HwpRecord_CtrlHeader(int tagNum, int level, int size) { super(tagNum, level, size); } public static Ctrl parse(int tagNum, int level, int size, byte[] buf, int off, int version) throws HwpParseException { int offset = off; // hwp포맷에는 역순으로 ctrlId를 구성한다. 여기서는 순방향으로 구성한다. String ctrlId = new String(buf, offset, 4, StandardCharsets.US_ASCII); offset += 4; Ctrl ctrl = null; log.fine(" ctrlID="+ctrlId); // ctrlId를 거꾸로 읽어 비교한다. switch(ctrlId) { case "dces": // 구역 정의 ctrl = new Ctrl_SectionDef(ctrlId, size-(offset-off), buf, offset, version); offset += ctrl.getSize(); break; case "dloc": ctrl = new Ctrl_ColumnDef(ctrlId, size-(offset-off), buf, offset, version); offset += ctrl.getSize(); break; case "daeh": // 머리말 ctrl = new Ctrl_HeadFoot(ctrlId, size-(offset-off), buf, offset, version, true); offset += ctrl.getSize(); break; case "toof": // 꼬리말 ctrl = new Ctrl_HeadFoot(ctrlId, size-(offset-off), buf, offset, version, false); offset += ctrl.getSize(); break; case " nf": // 각주 case " ne": // 미주 ctrl = new Ctrl_Note(ctrlId, size-(offset-off), buf, offset, version); offset += ctrl.getSize(); break; case " lbt": // table ctrl = new Ctrl_Table(ctrlId, size-(offset-off), buf, offset, version); offset += ctrl.getSize(); break; case "onta": // 자동 번호 ctrl = new Ctrl_AutoNumber(ctrlId, size-(offset-off), buf, offset, version); offset += ctrl.getSize(); break; case "onwn": // 새 번호 지정 ctrl = new Ctrl_NewNumber(ctrlId, size-(offset-off), buf, offset, version); offset += ctrl.getSize(); break; case " osg": // GeneralShapeObject ctrl = new Ctrl_GeneralShape(ctrlId, size-(offset-off), buf, offset, version); offset += ctrl.getSize(); break; case "deqe": ctrl = new Ctrl_EqEdit(ctrlId, size-(offset-off), buf, offset, version); offset += ctrl.getSize(); break; case "dhgp": // 감추기 { int tmpSize = size-(offset-off); ctrl = new Ctrl(ctrlId) { public int getSize() { return tmpSize; } }; ctrl.ctrlId = ctrlId; offset += ctrl.getSize(); ctrl.fullfilled = true; } log.fine("Known ctrlID="+ctrlId+", but is not implemented. Just skipping..."); break; case "cot%": // table of content { // 내용을 UTF_16LE로 읽었을때 아래 내용 같음. // ¥TableOfContents:set:140:ContentsMake:uint:17 ContentsStyles:wstring:0: ContentsLevel:int:5 ContentsAutoTabRight:int:0 ContentsLeader:int:3 ContentsHyperlink:bool:1 int tmpSize = size-(offset-off); // offset+=5; // String text = new String(buf, offset, tmpSize-13, StandardCharsets.UTF_16LE); // log.finest("TableOfContent:"+text); ctrl = new Ctrl(ctrlId) { public int getSize() { return tmpSize; } }; ctrl.ctrlId = ctrlId; offset += ctrl.getSize(); ctrl.fullfilled = true; } break; case "klc%": // FIELD_CLICKHERE ctrl = new Ctrl_Click(ctrlId, size-(offset-off), buf, offset, version); offset += ctrl.getSize(); ctrl.fullfilled = true; break; case "mrof": // 양식개체 ctrl = new Ctrl_Form(ctrlId, size-(offset-off), buf, offset, version); offset += ctrl.getSize(); ctrl.fullfilled = true; break; case "pngp": // 쪽 번호 위치 ctrl = new Ctrl_PageNumPos(ctrlId, size-(offset-off), buf, offset, version); offset += ctrl.getSize(); ctrl.fullfilled = true; break; case "klh%": // hyperlink case "frx%": // FIELD_CROSSREF case "knu%": // FIELD_UNKNOWN case "etd%": // FIELD_DATE case "tdd%": // FIELD_DOCDATE case "tap%": // FIELD_PATH case "kmb%": // FIELD_BOOKMARK case "gmm%": // FIELD_MAILMERGE case "umf%": // FIELD_FORMULA case "mxdi": // ??? case "mkob": // ??? case "spct": // ??? case "tmct": // ??? case "tcgp": // ??? case "tudt": // ??? default: { int tmpSize = size-(offset-off); ctrl = new Ctrl(ctrlId) { public int getSize() { return tmpSize; } }; ctrl.ctrlId = ctrlId; offset += ctrl.getSize(); ctrl.fullfilled = true; } log.fine("Known ctrlID="+ctrlId+", but is not implemented. Just skipping..."); break; } return ctrl; } } H2Orestart-0.7.2/source/HwpDoc/HwpElement/HwpRecord_DocumentProperties.java000066400000000000000000000112111476273367000267370ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.HwpElement; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import HwpDoc.HwpDocInfo; import HwpDoc.Exception.HwpParseException; public class HwpRecord_DocumentProperties extends HwpRecord { private HwpDocInfo parent; public short sectionSize; // 구역 갯수 public short pageStartNo; // 페이지 시작 번호 public short footNoteStartNo; // 각주 시작 번호 public short endNoteStartNo; // 미주 시작 번호 public short figureStartNo; // 그림 시작 번호 public short tableStartNo; // 표 시작 번호 public short eqStartNo; // 수식 시작 번호 public int listID; // 리스트 아이디 (문서 내 캐럿의 위치 정보) public int paraID; // 문단 아이디 (문서 내 캐럿의 위치 정보) public int charUnitLocInPara; // 문단 내에서의 글자 단위 위치 HwpRecord_DocumentProperties(int tagNum, int level, int size) { super(tagNum, level, size); } public HwpRecord_DocumentProperties(HwpDocInfo docInfo, int tagNum, int level, int size, byte[] buf, int off, int version) throws HwpParseException { this(tagNum, level, size); this.parent = docInfo; int offset = off; sectionSize = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; pageStartNo = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; footNoteStartNo = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; endNoteStartNo = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; figureStartNo = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; tableStartNo = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; eqStartNo = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; listID = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; paraID = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; charUnitLocInPara= buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; if (offset-off!=26) { throw new HwpParseException(); } } public HwpRecord_DocumentProperties(HwpDocInfo docInfo, Node node, int version) throws HwpParseException { super(HwpTag.HWPTAG_DOCUMENT_PROPERTIES, 0, 0); this.parent = docInfo; NamedNodeMap attributes = node.getAttributes(); // [endnote="1", equation="1", footnote="1", page="1", pic="1", tbl="1"] for (int i=0; i */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.HwpElement; import java.nio.charset.StandardCharsets; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.HwpDocInfo; import HwpDoc.Exception.HwpParseException; public class HwpRecord_FaceName extends HwpRecord { private static final Logger log = Logger.getLogger(HwpRecord_FaceName.class.getName()); private HwpDocInfo parent; public boolean basicFaceExists; // 속성 - 기본글꼴 존재여부 public boolean attrExists; // 속성 - 글꼴 유형정보 존재여부 public boolean substExists; // 속성 - 대체 글꼴 존재 여부 public String faceName; // 글꼴 이름 public AltType substType; // 대체 글꼴 유형 public String substFace; // 대체 글꼴 이름 public String basicFaceName; // 기본 글꼴 이름 public byte familyType; // 글꼴 유형정보 - 글꼴 계열 public byte serifStyle; // 글꼴 유형정보 - 세리프 유형 public short weight; // 글꼴 유형정보 - 굵기 public short propotion; // 글꼴 유형정보 - 비례 public short contrast; // 글꼴 유형정보 - 대조 public short strokeVariation; // 글꼴 유형정보 - 스트로크 편차 public short armStyle; // 글꼴 유형정보 - 자획유형 public short letterform; // 글꼴 유형정보 - 글자형 public short midLine; // 글꼴 유형정보 - 중간선 public short xHeight; // 글꼴 유형정보 - X-높이 HwpRecord_FaceName(int tagNum, int level, int size) { super(tagNum, level, size); } public HwpRecord_FaceName(HwpDocInfo docInfo, int tagNum, int level, int size, byte[] buf, int off, int version) throws HwpParseException { this(tagNum, level, size); this.parent = docInfo; int offset = off; basicFaceExists = (buf[offset]&0x20)==0x20?true:false; attrExists = (buf[offset]&0x40)==0x40?true:false; substExists = (buf[offset]&0x80)==0x80?true:false; offset += 1; int faceNameLen = 0; faceNameLen = (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF)*2; offset += 2; if (faceNameLen > 0) { faceName = new String(buf, offset, faceNameLen, StandardCharsets.UTF_16LE); offset += faceNameLen; } if (substExists) { substType = AltType.from(buf[offset++]&0x0F); faceNameLen = (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF)*2; offset += 2; if (faceNameLen > 0) { substFace = new String(buf, offset, faceNameLen, StandardCharsets.UTF_16LE); offset += faceNameLen; } } if (attrExists) { familyType = buf[offset++]; // 글꼴 유형정보 - 글꼴 계열 serifStyle = buf[offset++]; // 글꼴 유형정보 - 세리프 유형 weight = buf[offset++]; // 글꼴 유형정보 - 굵기 propotion = buf[offset++]; // 글꼴 유형정보 - 비례 contrast = buf[offset++]; // 글꼴 유형정보 - 대조 strokeVariation = buf[offset++]; // 글꼴 유형정보 - 스트로크 편차 armStyle = buf[offset++]; // 글꼴 유형정보 - 자획유형 letterform = buf[offset++]; // 글꼴 유형정보 - 글자형 midLine = buf[offset++]; // 글꼴 유형정보 - 중간선 xHeight = buf[offset++]; // 글꼴 유형정보 - X-높이 } if (basicFaceExists) { faceNameLen = (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF)*2; offset += 2; if (faceNameLen > 0) { basicFaceName = new String(buf, offset, faceNameLen, StandardCharsets.UTF_16LE); offset += faceNameLen; } } log.fine(" " +"ID="+(parent.faceNameList.size()) +",Nm="+faceName +(basicFaceName==null?"":",Basic="+basicFaceName) +(substExists?",Alt="+substFace+( substType==null?"":"("+substType.toString()+")"):"") +(attrExists==false?"":",계열="+familyType) +(attrExists==false?"":",세리프="+serifStyle) +(attrExists==false?"":",굵기="+weight) +(attrExists==false?"":",비례="+propotion) +(attrExists==false?"":",대조="+contrast) +(attrExists==false?"":",스트로크편차="+strokeVariation) +(attrExists==false?"":",자획유형="+armStyle) +(attrExists==false?"":",글자형="+letterform) +(attrExists==false?"":",중간선="+midLine) +(attrExists==false?"":",X높이="+xHeight)); if (offset-off-size != 0) { throw new HwpParseException(); } } public HwpRecord_FaceName(HwpDocInfo docInfo, Node node, int version) throws HwpParseException { super(HwpTag.HWPTAG_FACE_NAME, 0, 0); this.parent = docInfo; // TagName = hh:font // attributes = [face="돋움", id="0", isEmbedded="0", type="TTF"] // children = hh:typeInfo // [armStyle="1", contrast="0", familyType="FCAT_GOTHIC", letterform="1", midline="1", proportion="0", strokeVariation="1", weight="6", xHeight="1"] NamedNodeMap childAttrs = node.getAttributes(); for (int j=0; j */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.HwpElement; import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; import HwpDoc.HwpDocInfo; import HwpDoc.Exception.HwpParseException; public class HwpRecord_IdMapping extends HwpRecord { private static final Logger log = Logger.getLogger(HwpRecord_IdMapping.class.getName()); HwpDocInfo parent; public List idMappingNum; // 아이디 매핑 개수 private int[] counts = new int[Index.MAX.index]; HwpRecord_IdMapping(int tagNum, int level, int size) { super(tagNum, level, size); idMappingNum = new ArrayList(); } public HwpRecord_IdMapping(HwpDocInfo docInfo, int tagNum, int level, int size, byte[] buf, int off, int version) throws HwpParseException { this(tagNum, level, size); this.parent = docInfo; int offset = off; for (int i=0; i<(size/4); i++) { int count = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; idMappingNum.add(count); offset += 4; counts[i] = count; log.finest("Total " + count + " IDs are mapping to " + Index.from(i)); switch(Index.from(i)) { case BIN_DATA: if (parent.getParentHwp().getBinData()==null) parent.getParentHwp().setBinData(parent.getParentHwp().getOleFile().getChildEntries("BinData")); if (count > parent.getParentHwp().getBinData().size()) { log.fine("BIN_DATA count mismatch"); } break; default: } } log.fine(" " +"BinData="+counts[Index.BIN_DATA.index] +",한글Font="+counts[Index.FACENAME_HANGUL.index] +",BorderFill="+counts[Index.BORDER_FILL.index] +",CharShape="+counts[Index.CHAR_SHAPE.index] +",TabDef="+counts[Index.TAB_DEF.index] +",Numbering="+counts[Index.NUMBERING.index] +",Bullet="+counts[Index.BULLET.index] +",ParaShape="+counts[Index.PARA_SHAPE.index] +",Style="+counts[Index.STYLE.index] ); if (offset-off-size!=0) { throw new HwpParseException(); } } public static enum Index { BIN_DATA (0), FACENAME_HANGUL (1), FACENAME_ENGLISH (2), FACENAME_CHINESE (3), FACENAME_JAPANESE (4), FACENAME_ETC (5), FACENAME_SYMBOL (6), FACENAME_USER (7), BORDER_FILL (8), CHAR_SHAPE (9), TAB_DEF (10), NUMBERING (11), BULLET (12), PARA_SHAPE (13), STYLE (14), MEMO_SHAPE (15), TRACK_CHANGE (16), TRACK_CHANGE_USER (17), MAX (18); private int index; private Index(int index) { this.index = index; } public static Index from(int index) { for (Index indexNum: values()) { if (indexNum.index == index) return indexNum; } return null; } } } H2Orestart-0.7.2/source/HwpDoc/HwpElement/HwpRecord_ListHeader.java000066400000000000000000000043471476273367000251440ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.HwpElement; import java.util.logging.Logger; import HwpDoc.Exception.HwpParseException; import HwpDoc.paragraph.Ctrl_Common.VertAlign; public class HwpRecord_ListHeader extends HwpRecord { private static final Logger log = Logger.getLogger(HwpRecord_ListHeader.class.getName()); HwpRecord_ListHeader(int tagNum, int level, int size) { super(tagNum, level, size); } public static int getCount(int tagNum, int level, int size, byte[] buf, int off, int version) throws HwpParseException { int offset = off; short nParas = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; log.fine(" "+"문단갯수="+nParas); return nParas; } public static VertAlign getVertAlign(int size, byte[] buf, int off, int version) throws HwpParseException { int offset = off; offset += 2; int attr = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; VertAlign verAlign = VertAlign.from(attr>>21&0x03); log.fine(" " +"리스트헤더속성="+String.format("0x%X", attr) +",세로정렬="+verAlign.toString()); return verAlign; } } H2Orestart-0.7.2/source/HwpDoc/HwpElement/HwpRecord_Numbering.java000066400000000000000000000477771476273367000250640ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.HwpElement; import java.nio.charset.StandardCharsets; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.HwpDocInfo; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; public class HwpRecord_Numbering extends HwpRecord { private static final Logger log = Logger.getLogger(HwpRecord_Numbering.class.getName()); private HwpDocInfo parent; public Numbering[] numbering = new Numbering[10]; // 문단머리정보+번호형식[1~7] public short start; // 시작번호 public String[] extLevelFormat = new String[3]; // 확장 번호 형식 public int[] extLevelStart = new int[3]; // 확장 수준별 시작번호 HwpRecord_Numbering(int tagNum, int level, int size) { super(tagNum, level, size); } public HwpRecord_Numbering(HwpDocInfo docInfo, int tagNum, int level, int size, byte[] buf, int off, int version) throws HwpParseException { this(tagNum, level, size); this.parent = docInfo; int offset = off; for (int i=0; i < 7; i++) { numbering[i] = new Numbering(); int typeBits = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; numbering[i].align = (byte) ((typeBits)&0x03); numbering[i].useInstWidth = (typeBits&0x40)==0x40?true:false; numbering[i].autoIndent = (typeBits&0x80)==0x80?true:false; numbering[i].textOffsetType = (byte) ((typeBits>>>4)&0x01); numbering[i].widthAdjust = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; numbering[i].textOffset = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; numbering[i].charShape = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; short len = (short) ((buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF)*2); offset += 2; numbering[i].numFormat = new String(buf, offset, len, StandardCharsets.UTF_16LE); offset += len; } // 의 "start" 속성에 대응 start = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; if (version > 5025 && offset-off < size) { // 하위 태그의 "start" 속성에 대응 for (int i=0; i < 7; i++) { numbering[i].startNumber = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; } } if (version > 5100 && offset-off < size) { for (int i=0; i < 3; i++) { // 내용은 알수 없으나, 8byte를 포함하고 있음. offset += 8; // 내용은 알수 없으나, 4byte를 포함하고 있음. offset += 4; // 내용을 알수 없으나, 글자수를 포함한것으로 보임. short len = (short) ((buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF)*2); offset += 2; // 글자수*2 만큼 건너뜀 offset += len; } for (int i=0; i < 3; i++) { extLevelStart[i] = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; } } log.fine(" " +"ID="+(parent.numberingList.size()+1) +",포맷1="+numbering[0].numFormat +(numbering[0].charShape!=-1?"("+((HwpRecord_CharShape)(parent.charShapeList.get(numbering[0].charShape))).fontName[0]+")":"") +",포맷2="+numbering[1].numFormat +(numbering[1].charShape!=-1?"("+((HwpRecord_CharShape)(parent.charShapeList.get(numbering[1].charShape))).fontName[0]+")":"") +",포맷3="+numbering[2].numFormat +(numbering[2].charShape!=-1?"("+((HwpRecord_CharShape)(parent.charShapeList.get(numbering[2].charShape))).fontName[0]+")":"") +",포맷4="+numbering[3].numFormat +(numbering[3].charShape!=-1?"("+((HwpRecord_CharShape)(parent.charShapeList.get(numbering[3].charShape))).fontName[0]+")":"") +",포맷5="+numbering[4].numFormat +(numbering[4].charShape!=-1?"("+((HwpRecord_CharShape)(parent.charShapeList.get(numbering[4].charShape))).fontName[0]+")":"") +",포맷6="+numbering[5].numFormat +(numbering[5].charShape!=-1?"("+((HwpRecord_CharShape)(parent.charShapeList.get(numbering[5].charShape))).fontName[0]+")":"") +",포맷7="+numbering[6].numFormat +(numbering[6].charShape!=-1?"("+((HwpRecord_CharShape)(parent.charShapeList.get(numbering[6].charShape))).fontName[0]+")":"") +",시작번호="+start +",수준별시작번호=("+numbering[0].startNumber+","+numbering[1].startNumber+","+numbering[2].startNumber+"," +numbering[3].startNumber+","+numbering[4].startNumber+","+numbering[5].startNumber+","+numbering[6].startNumber+")" ); if (offset-off-size != 0) { dump(buf, off, size); throw new HwpParseException(); } } public HwpRecord_Numbering(HwpDocInfo docInfo, Node node, int version) throws NotImplementedException { super(HwpTag.HWPTAG_NUMBERING, 0, 0); this.parent = docInfo; NamedNodeMap attributes = node.getAttributes(); // id값은 처리하지 않는다. List에 순차적으로 추가한다. // String id = attributes.getNamedItem("height").getNodeValue(); start = 1; String numStr = attributes.getNamedItem("start").getNodeValue(); start = (short) Integer.parseInt(numStr); NodeList nodeList = node.getChildNodes(); for (int i=0,j=0; i */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.HwpElement; import java.util.ArrayList; import java.util.logging.Logger; import HwpDoc.Exception.HwpParseException; import HwpDoc.paragraph.HwpParagraph; import HwpDoc.paragraph.RangeTag; public class HwpRecord_ParaRangeTag extends HwpRecord { private static final Logger log = Logger.getLogger(HwpRecord_ParaRangeTag.class.getName()); HwpRecord_ParaRangeTag(int tagNum, int level, int size) { super(tagNum, level, size); } public static int parse(HwpParagraph para, int tagNum, int level, int size, byte[] buf, int off, int version) throws HwpParseException { int offset = off; if (para.rangeTags==null) { para.rangeTags = new ArrayList(); } while(size-(offset-off) >= 12) { RangeTag rangeTag = new RangeTag(); rangeTag.startPos = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; rangeTag.endPos = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; rangeTag.tag = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; para.rangeTags.add(rangeTag); } if (offset-off-size != 0) { log.severe("[TAG]=" + tagNum + ", size=" + size + ", but currentSize=" + (offset-off)); dump(buf, off, size); // throw new HwpParseException(); } return size; } } H2Orestart-0.7.2/source/HwpDoc/HwpElement/HwpRecord_ParaShape.java000066400000000000000000000547051476273367000247670ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.HwpElement; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.HwpDocInfo; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; public class HwpRecord_ParaShape extends HwpRecord { private static final Logger log = Logger.getLogger(HwpRecord_ParaShape.class.getName()); private HwpDocInfo parent; public HorizontalAlign align; // 정렬방식 (0:양쪽정렬, 1:왼쪽정렬, 2:오른쪽정렬, 3:가운데정렬, 4:배분정렬, 5:나눔 정렬 public byte breakLatinWord; // 줄 나눔 기준 영어 단위 (0:단어, 1:하이픈, 2:글자) public byte breakNonLatinWord; // 줄 나눔 기준 한글 단위 (0:어절, 1:글자) public boolean snapToGrid; // 편집 용지의 줄 격자 사용 여부 public byte condense; // 공백 최소값 (0%~75%) public boolean widowOrphan; // 외톨이줄 보호 여부 public boolean keepWithNext; // 다음 문단과 함께 여부 public boolean pageBreakBefore; // 문단 앞에서 항상 쪽 나눔 여부 public VerticalAlign vertAlign; // 세로정렬 (0:글꼴기준, 1:위쪽, 2:가운데, 3:아래) public boolean fontLineHeight; // 글꼴에 어울리는 줄 높이 여부 public HeadingType headingType; // 문단 머리 모양 종류 (0:없음, 1:개요, 2:번호, 3:글머리표(bullet)) public byte headingLevel; // 문단 수준 (1수준~7수준) public boolean connect; // 문단 테두리 연결 여부 public boolean ignoreMargin; // 문단 여백 무시 여부 public boolean paraTailShape; // 문단 꼬리 모양 public int indent; // 들여쓰기/내어쓰기. public int marginLeft; // 왼쪽 여백 HWPUINT형이 아닌 INT32형이다. 7000값을 35pt, 12.3mm로 계산한다. public int marginRight; // 오른쪽 여백 HWPUINT형이 아닌 INT32형이다. 7000값을 35pt, 12.3mm로 계산한다. public int marginPrev; // 문단 간격 위 HWPUINT형이 아닌 INT32형이다. 7000값을 35pt, 12.3mm로 계산한다. public int marginNext; // 문단 간격 아래 HWPUINT형이 아닌 INT32형이다. 7000값을 35pt, 12.3mm로 계산한다. public int lineSpacing; // 줄 간격. 한글2007 이하버전(5.0.2.5 버전 미만)에서 사용. // percent일때:0%~500%, fixed일때:hpwunit또는 글자수,betweenline일때:hwpunit또는글자수 public short tabDef; // 탭 정의 아이디(TabDef ID) 참조 값 public short headingIdRef; // 번호 문단 ID(Numbering ID) 또는 글머리표 문단 모양 ID(Bullet ID)참조 값 public short borderFill; // 테두리/배경 모양 ID(BorderFill ID) 참조 값 public short offsetLeft; // 문단 테두리 왼쪽 간격 public short offsetRight; // 문단 테두리 오른쪽 간격 public short offsetTop; // 문단 테두리 위쪽 간격 public short offsetBottom; // 문단 테두리 아래쪽 간격 // 속성2 (5.0.1.7 버전 이상) public byte lineWrap; // 한줄로 입력 public boolean autoSpaceEAsianEng; // 한글과 영어 간격을 자동 조절 public boolean autoSpaceEAsianNum; // 한글과 숫자 간격을 자동 조절 // 속성3 (5.0.2.5 버전 이상) public int lineSpacingType; // 줄간격 종류(0:Percent,1:Fixed,2:BetweenLines,4:AtLeast) public boolean firstAfterTable; // 테이블 다음에 첫문단인지 여부 (리브레오피스와 한컴오피스의 테이블 차이를 극복하기 위한 속성 정의 HwpRecord_ParaShape(int tagNum, int level, int size) { super(tagNum, level, size); } public HwpRecord_ParaShape(HwpDocInfo docInfo, int tagNum, int level, int size, byte[] buf, int off, int version) throws HwpParseException { super(tagNum, level, size); this.parent = docInfo; int offset = off; int typeBits = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; align = HorizontalAlign.from(typeBits>>>2 & 0x07); breakLatinWord = (byte) (typeBits>>>5 & 0x03); breakNonLatinWord = (byte) (typeBits>>>7 & 0x01); snapToGrid = (typeBits&0x80)==0x80?true:false; condense = (byte) (typeBits>>>9 & 0x7F); widowOrphan = (typeBits&0x10000)==0x10000?true:false; keepWithNext = (typeBits&0x20000)==0x20000?true:false; pageBreakBefore = (typeBits&0x40000)==0x40000?true:false; vertAlign = VerticalAlign.from(typeBits>>>20 & 0x03); fontLineHeight = (typeBits&0x100000)==0x100000?true:false; headingType = HeadingType.from((typeBits>>>23 & 0x03)); headingLevel = (byte) (typeBits>>>25 & 0x07); connect = (typeBits&0x800000)==0x800000?true:false; ignoreMargin = (typeBits&0x1000000)==0x1000000?true:false; paraTailShape = (typeBits&0x2000000)==0x2000000?true:false; marginLeft = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; marginRight = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; indent = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; marginPrev = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; marginNext = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; if (version<5025) { lineSpacingType = (byte) (typeBits & 0x03); lineSpacing = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; } else { offset += 4; } tabDef = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; headingIdRef = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; borderFill = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; offsetLeft = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; offsetRight = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; offsetTop = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; offsetBottom = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; if (version>=5017) { int attrBits = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; lineWrap = (byte) (attrBits & 0x03); autoSpaceEAsianEng =(typeBits&0x10)==0x10?true:false; autoSpaceEAsianNum =(typeBits&0x20)==0x20?true:false; } else { offset += 4; } if (version>=5025) { int attrBits = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; lineSpacingType = (byte) (attrBits&0x0F); lineSpacing = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; } else { offset += 8; } log.fine(" " +"ID="+(parent.paraShapeList.size()) +",정렬="+align+",줄나눔="+breakNonLatinWord +",문단margin=("+marginLeft+","+marginRight+","+marginPrev+","+marginNext+")" +",문단offset=("+offsetLeft+","+offsetRight+","+offsetTop+","+offsetBottom+")" +",줄간격="+lineSpacingType+":"+lineSpacing +","+headingType+"(ID="+headingIdRef+",Level="+headingLevel+")" +",탭ID="+tabDef +",테두리ID="+borderFill ); if (offset-off-size != 0 && offset-off!=54) { log.fine("[TAG]=" + tag.toString() + ", size=" + size + ", but currentSize=" + (offset-off)); dump(buf, off, size); throw new HwpParseException(); } } public HwpRecord_ParaShape(HwpDocInfo docInfo, Node node, int version) throws NotImplementedException { super(HwpTag.HWPTAG_PARA_SHAPE, 0, 0); this.parent = docInfo; NamedNodeMap attributes = node.getAttributes(); // 초기값 headingType = HeadingType.NONE; align = HorizontalAlign.JUSTIFY; vertAlign = VerticalAlign.BASELINE; // id값은 처리하지 않는다. List에 순차적으로 추가한다. // String id = attributes.getNamedItem("id").getNodeValue(); String numStr = attributes.getNamedItem("tabPrIDRef").getNodeValue(); tabDef = (short) Integer.parseInt(numStr); numStr = attributes.getNamedItem("condense").getNodeValue(); condense = (byte) Integer.parseInt(numStr); switch(attributes.getNamedItem("fontLineHeight").getNodeValue()) { case "0": fontLineHeight = false; break; case "1": fontLineHeight = true; break; } switch(attributes.getNamedItem("snapToGrid").getNodeValue()) { case "0": snapToGrid = false; break; case "1": snapToGrid = true; break; } /* switch(attributes.getNamedItem("suppressLineNumbers").getNodeValue()) { case "0": suppressLineNumbers = false; break; case "1": suppressLineNumbers = true; break; } */ /* switch(attributes.getNamedItem("checked").getNodeValue()) { case "0": checked = false; break; case "1": checked = true; break; } */ NodeList nodeList = node.getChildNodes(); for (int i=0; i */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.HwpElement; import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import HwpDoc.Exception.HwpParseException; import HwpDoc.paragraph.Ctrl; import HwpDoc.paragraph.Ctrl_AutoNumber; import HwpDoc.paragraph.Ctrl_Character; import HwpDoc.paragraph.Ctrl_Character.CtrlCharType; import HwpDoc.paragraph.Ctrl_ColumnDef; import HwpDoc.paragraph.Ctrl_EqEdit; import HwpDoc.paragraph.Ctrl_GeneralShape; import HwpDoc.paragraph.Ctrl_HeadFoot; import HwpDoc.paragraph.Ctrl_NewNumber; import HwpDoc.paragraph.Ctrl_Note; import HwpDoc.paragraph.Ctrl_PageNumPos; import HwpDoc.paragraph.Ctrl_SectionDef; import HwpDoc.paragraph.Ctrl_Table; import HwpDoc.paragraph.ParaText; public class HwpRecord_ParaText extends HwpRecord { private static final Logger log = Logger.getLogger(HwpRecord_ParaText.class.getName()); private static final String PATTERN_STRING = "[\\u0000\\u000a\\u000d\\u0018-\\u001f]|[\\u0001\\u0002-\\u0009\\u000b-\\u000c\\u000e-\\u0017].{6}[\\u0001\\u0002-\\u0009\\u000b-\\u000c\\u000e-\\u0017]"; private static final String PATTERN_8BYTES = "[\\u0001\\u0002-\\u0009\\u000b-\\u000c\\u000e-\\u0017].{6}[\\u0001\\u0002-\\u0009\\u000b-\\u000c\\u000e-\\u0017]"; public static Pattern pattern = Pattern.compile(PATTERN_STRING); HwpRecord_ParaText(int tagNum, int level, int size) { super(tagNum, level, size); } public static List parse(int tagNum, int level, int size, byte[] buf, int off, int version) throws HwpParseException { int offset = off; ArrayList paras = new ArrayList<>(); String text = new String(buf, offset, size, StandardCharsets.UTF_16LE); offset += size; Matcher m = pattern.matcher(text); log.finer("paraText Length="+ text.length()); int prevIndex = 0; while(m.find()) { if (m.start()>prevIndex) { // write text int startIndex = prevIndex; String content = text.substring(startIndex, m.start()); paras.add(new ParaText("____", content, startIndex)); } if (m.start()+1==m.end()) { // 문자컨드롤 byte controlByte = m.group().getBytes(StandardCharsets.UTF_16LE)[0]; switch(controlByte) { case 0x0a: // 10 한 줄 끝 (line break); paras.add(new Ctrl_Character(" _", CtrlCharType.LINE_BREAK)); break; case 0x0d: // 13 문단 끝 (para break) paras.add(new Ctrl_Character(" _", CtrlCharType.PARAGRAPH_BREAK)); break; case 0x18: // 24 하이픈 paras.add(new Ctrl_Character(" _", CtrlCharType.HARD_HYPHEN)); break; case 0x1e: // 30 묶음 빈칸 case 0x1f: // 31 고정폭 빈칸 paras.add(new Ctrl_Character(" _", CtrlCharType.HARD_SPACE)); break; } } else if (m.start()+8==m.end()) { // 인라인 컨트롤, 확장컨트롤 byte controlByte = m.group().getBytes(StandardCharsets.UTF_16LE)[0]; String info = new String(m.group().getBytes(StandardCharsets.UTF_16LE), 2, 12, StandardCharsets.US_ASCII).replaceAll("[\\x00-\\x20]+$", ""); switch(controlByte) { case 0x04: // 필드 끝 break; case 0x08: // title mark break; case 0x09: // 탭 paras.add(new ParaText("____", "\t", 0)); break; case 0x10: // 머리말/꼬리말 paras.add(new Ctrl_HeadFoot(info)); break; case 0x12: // 자동번호 paras.add(new Ctrl_AutoNumber(info)); break; case 0x15: // 페이지 컨트롤(감추기, 새번호로 시작 등) { switch(info) { case "dhgp": // 감추기 break; case "pngp": // 쪽 번호 위치 paras.add(new Ctrl_PageNumPos(info)); break; case "onwn": // 새 번호 지정 paras.add(new Ctrl_NewNumber(info)); break; } } break; case 0x02: // 구역정의/단정의 { switch(info) { case "dces": paras.add(new Ctrl_SectionDef(info)); break; case "dloc": paras.add(new Ctrl_ColumnDef(info)); break; } } break; case 0x03: // 필드 시작 (누름틀,하이퍼링크,블록책갈피,표계산식,문서 요약,사용자 정보,현재 날짜/시간,문서 날짜/시간,파일 경로,상호 참조,메일머지,메모,교정부호,개인정보 case 0x0e: // 예약 case 0x0f: // 숨은 설명 break; case 0x11: // 각주/미주 paras.add(new Ctrl_Note(info)); break; case 0x16: // 책갈피/찾아보기 표식 case 0x17: // 덧말/글자 겹침 break; case 0x0b: // 그리기 개체/표 { switch(info) { case " osg": paras.add(new Ctrl_GeneralShape(info)); break; case " lbt": paras.add(new Ctrl_Table(info)); break; case "deqe": paras.add(new Ctrl_EqEdit(info)); break; case "mrof": break; } } break; default: break; } } prevIndex = m.end(); } if (prevIndex */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.HwpElement; import java.nio.charset.StandardCharsets; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import HwpDoc.HwpDocInfo; import HwpDoc.Exception.HwpParseException; public class HwpRecord_Style extends HwpRecord { private static final Logger log = Logger.getLogger(HwpRecord_Style.class.getName()); private HwpDocInfo parent; public String name; // 로컬 스타일 이름. 한글 윈도우에서는 한글 스타일 이름 public String engName; // 영문 스타일 이름. public byte type; // 속성 public byte nextStyle; // 다음 스타일 아이디 참조값 public short langId; // 언어 아이디 public int paraShape; // 문단 모양 아이디 참조값(문단 모양의 아이디 속성) public int charShape; // 글자 모양 아이디 참조값(글자 모양의 아이디 속성) public boolean lockForm; // 양식모드에서 style 보호하기 HwpRecord_Style(int tagNum, int level, int size) { super(tagNum, level, size); } public HwpRecord_Style(HwpDocInfo docInfo, int tagNum, int level, int size, byte[] buf, int off, int version) throws HwpParseException { this(tagNum, level, size); this.parent = docInfo; int offset = off; int styleNameLen1 = (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF)*2; offset += 2; if (styleNameLen1 > 0) { name = new String(buf, offset, styleNameLen1, StandardCharsets.UTF_16LE); offset += styleNameLen1; } int styleNameLen2 = (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF)*2; offset += 2; if (styleNameLen2 > 0) { engName = new String(buf, offset, styleNameLen2, StandardCharsets.UTF_16LE); offset += styleNameLen2; } type = (byte) (buf[offset++]&0x00FF); nextStyle = (byte) (buf[offset++]&0x00FF); langId = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; paraShape = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; charShape = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; log.fine(" " +"ID="+(parent.styleList.size()) +",스타일="+name +",스타일구분="+(type==0?"문단스타일":type==1?"글자스타일":"알수없음") +",문단모양ID="+paraShape +",글자모양ID="+charShape ); if (offset-off-size!=0 && offset-off!=12+styleNameLen1+styleNameLen2) { log.fine("[TAG]=" + tag.toString() + ", size=" + size + ", but currentSize=" + (offset-off)); dump(buf, off, size); throw new HwpParseException(); } } public HwpRecord_Style(HwpDocInfo docInfo, Node node, int version) { super(HwpTag.HWPTAG_STYLE, 0, 0); this.parent = docInfo; NamedNodeMap attributes = node.getAttributes(); // id값은 처리하지 않는다. List에 순차적으로 추가한다. // String id = attributes.getNamedItem("id").getNodeValue(); switch(attributes.getNamedItem("type").getNodeValue()) { case "PARA": type = 0; break; case "CHAR": type = 1; break; } name = attributes.getNamedItem("name").getNodeValue(); engName = attributes.getNamedItem("engName").getNodeValue(); String numStr = attributes.getNamedItem("paraPrIDRef").getNodeValue(); paraShape = Integer.parseUnsignedInt(numStr); numStr = attributes.getNamedItem("charPrIDRef").getNodeValue(); charShape = Integer.parseUnsignedInt(numStr); numStr = attributes.getNamedItem("nextStyleIDRef").getNodeValue(); nextStyle = (byte) Integer.parseInt(numStr); numStr = attributes.getNamedItem("langID").getNodeValue(); langId = (short) Integer.parseInt(numStr); switch(attributes.getNamedItem("lockForm").getNodeValue()) { case "0": lockForm = false; break; case "1": lockForm = true; break; } } } H2Orestart-0.7.2/source/HwpDoc/HwpElement/HwpRecord_TabDef.java000066400000000000000000000165651476273367000242520ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.HwpElement; import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.HwpDocInfo; import HwpDoc.Exception.HwpParseException; import HwpDoc.HwpElement.HwpRecordTypes.LineStyle2; public class HwpRecord_TabDef extends HwpRecord { private static final Logger log = Logger.getLogger(HwpRecord_TabDef.class.getName()); private HwpDocInfo parent; public int attr; public int count; public List tabs; HwpRecord_TabDef(int tagNum, int level, int size) { super(tagNum, level, size); } public HwpRecord_TabDef(HwpDocInfo docInfo, int tagNum, int level, int size, byte[] buf, int off, int version) throws HwpParseException { this(tagNum, level, size); this.parent = docInfo; int offset = off; attr = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; count = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; if (size-(offset-off)!=count*8) { throw new HwpParseException(); } tabs = new ArrayList(); for (int i=0; i0?",탭[0]="+tabs.get(0).pos+":"+tabs.get(0).type.toString()+":"+tabs.get(0).leader:"") +(count>1?",탭[1]="+tabs.get(1).pos+":"+tabs.get(1).type.toString()+":"+tabs.get(1).leader:"") +(count>2?",탭[2]="+tabs.get(2).pos+":"+tabs.get(2).type.toString()+":"+tabs.get(2).leader:"") +",왼쪽끝자동="+(attr&0x1)+",오른쪽끝자동="+(attr&0x2) ); if (offset-off-size!=0) { throw new HwpParseException(); } } public HwpRecord_TabDef(HwpDocInfo docInfo, Node node, int version) { super(HwpTag.HWPTAG_TAB_DEF, 0, 0); this.parent = docInfo; NamedNodeMap attributes = node.getAttributes(); // id는 처리하지 않는다. List에 순차적으로 추가한다. // String id = attributes.getNamedItem("id").getNodeValue(); switch(attributes.getNamedItem("autoTabLeft").getNodeValue()) { case "0": attr &= 0xFFFFFFFE; break; case "1": attr |= 0x00000001; break; } switch(attributes.getNamedItem("autoTabRight").getNodeValue()) { case "0": attr &= 0xFFFFFFFD; break; case "1": attr |= 0x00000002; break; } String numStr = null; tabs = new ArrayList(); NodeList nodeList = node.getChildNodes(); for (int i=0; i */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.HwpElement; class _HwpTag { final static int HWPTAG_BEGIN = 0x010; } public enum HwpTag { HWPTAG_DOCUMENT_PROPERTIES (_HwpTag.HWPTAG_BEGIN), HWPTAG_ID_MAPPINGS (_HwpTag.HWPTAG_BEGIN+1), HWPTAG_BIN_DATA (_HwpTag.HWPTAG_BEGIN+2), HWPTAG_FACE_NAME (_HwpTag.HWPTAG_BEGIN+3), HWPTAG_BORDER_FILL (_HwpTag.HWPTAG_BEGIN+4), HWPTAG_CHAR_SHAPE (_HwpTag.HWPTAG_BEGIN+5), HWPTAG_TAB_DEF (_HwpTag.HWPTAG_BEGIN+6), HWPTAG_NUMBERING (_HwpTag.HWPTAG_BEGIN+7), HWPTAG_BULLET (_HwpTag.HWPTAG_BEGIN+8), HWPTAG_PARA_SHAPE (_HwpTag.HWPTAG_BEGIN+9), HWPTAG_STYLE (_HwpTag.HWPTAG_BEGIN+10), HWPTAG_DOC_DATA (_HwpTag.HWPTAG_BEGIN+11), HWPTAG_DISTRIBUTE_DOC_DATA (_HwpTag.HWPTAG_BEGIN+12), HWPTAG_COMPATIBLE_DOCUMENT (_HwpTag.HWPTAG_BEGIN+14), HWPTAG_LAYOUT_COMPATIBILITY (_HwpTag.HWPTAG_BEGIN+15), HWPTAG_TRACKCHANGE (_HwpTag.HWPTAG_BEGIN+16), HWPTAG_PARA_HEADER (_HwpTag.HWPTAG_BEGIN+50), HWPTAG_PARA_TEXT (_HwpTag.HWPTAG_BEGIN+51), HWPTAG_PARA_CHAR_SHAPE (_HwpTag.HWPTAG_BEGIN+52), HWPTAG_PARA_LINE_SEG (_HwpTag.HWPTAG_BEGIN+53), HWPTAG_PARA_RANGE_TAG (_HwpTag.HWPTAG_BEGIN+54), HWPTAG_CTRL_HEADER (_HwpTag.HWPTAG_BEGIN+55), HWPTAG_LIST_HEADER (_HwpTag.HWPTAG_BEGIN+56), HWPTAG_PAGE_DEF (_HwpTag.HWPTAG_BEGIN+57), HWPTAG_FOOTNOTE_SHAPE (_HwpTag.HWPTAG_BEGIN+58), HWPTAG_PAGE_BORDER_FILL (_HwpTag.HWPTAG_BEGIN+59), HWPTAG_SHAPE_COMPONENT (_HwpTag.HWPTAG_BEGIN+60), HWPTAG_TABLE (_HwpTag.HWPTAG_BEGIN+61), HWPTAG_SHAPE_COMPONENT_LINE (_HwpTag.HWPTAG_BEGIN+62), HWPTAG_SHAPE_COMPONENT_RECTANGLE(_HwpTag.HWPTAG_BEGIN+63), HWPTAG_SHAPE_COMPONENT_ELLIPSE (_HwpTag.HWPTAG_BEGIN+64), HWPTAG_SHAPE_COMPONENT_ARC (_HwpTag.HWPTAG_BEGIN+65), HWPTAG_SHAPE_COMPONENT_POLYGON (_HwpTag.HWPTAG_BEGIN+66), HWPTAG_SHAPE_COMPONENT_CURVE (_HwpTag.HWPTAG_BEGIN+67), HWPTAG_SHAPE_COMPONENT_OLE (_HwpTag.HWPTAG_BEGIN+68), HWPTAG_SHAPE_COMPONENT_PICTURE (_HwpTag.HWPTAG_BEGIN+69), HWPTAG_SHAPE_COMPONENT_CONTAINER(_HwpTag.HWPTAG_BEGIN+70), HWPTAG_CTRL_DATA (_HwpTag.HWPTAG_BEGIN+71), HWPTAG_EQEDIT (_HwpTag.HWPTAG_BEGIN+72), HWPTAG_SHAPE_COMPONENT_TEXTART (_HwpTag.HWPTAG_BEGIN+74), HWPTAG_FORM_OBJECT (_HwpTag.HWPTAG_BEGIN+75), HWPTAG_MEMO_SHAPE (_HwpTag.HWPTAG_BEGIN+76), HWPTAG_MEMO_LIST (_HwpTag.HWPTAG_BEGIN+77), HWPTAG_FORBIDDEN_CHAR (_HwpTag.HWPTAG_BEGIN+78), HWPTAG_CHART_DATA (_HwpTag.HWPTAG_BEGIN+79), HWPTAG_TRACK_CHANGE (_HwpTag.HWPTAG_BEGIN+80), HWPTAG_TRACK_CHANGE_AUTHOR (_HwpTag.HWPTAG_BEGIN+81), HWPTAG_VIDEO_DATA (_HwpTag.HWPTAG_BEGIN+82), HWPTAG_SHAPE_COMPONENT_UNKNOWN (_HwpTag.HWPTAG_BEGIN+99); private int tagNum; private HwpTag(int tagNum) { this.tagNum = tagNum; } private HwpTag(HwpTag tag) { this.tagNum = tag.tagNum; } public static HwpTag from(int tagNum) { for (HwpTag tag: values()) { if (tag.tagNum == tagNum) return tag; } return null; } } H2Orestart-0.7.2/source/HwpDoc/HwpElement/HwpType.java000066400000000000000000000020521476273367000225320ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.HwpElement; public class HwpType { HwpTag tag; } H2Orestart-0.7.2/source/HwpDoc/HwpFile.java000066400000000000000000000444721476273367000204340ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; import java.util.regex.Pattern; import java.util.stream.Stream; import java.util.zip.DataFormatException; import java.util.zip.Inflater; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.SecretKeySpec; import HwpDoc.HwpElement.HwpRecord_BinData.Compressed; import HwpDoc.Exception.CompoundDetectException; import HwpDoc.Exception.CompoundParseException; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; import HwpDoc.HwpElement.HwpTag; import HwpDoc.OLEdoc.CompoundFile; import HwpDoc.OLEdoc.DirectoryEntry; import HwpDoc.paragraph.HwpParagraph; public class HwpFile { private static final Logger log = Logger.getLogger(HwpFile.class.getName()); public String filename; public CompoundFile oleFile; public HwpFileHeader fileHeader; public int version; public HwpDocInfo docInfo; public List bodyText; public List viewText; // Let's have member that are needed for showing in LibreOffice public List directoryBinData; public List paraList; public HwpFile(String filename) throws FileNotFoundException { this.filename = filename; oleFile = new CompoundFile(this.filename); fileHeader = new HwpFileHeader(); docInfo = new HwpDocInfo(this); bodyText = new ArrayList(); viewText = new ArrayList(); } public HwpFile(File file) throws FileNotFoundException { oleFile = new CompoundFile(file); this.filename = file.toString(); fileHeader = new HwpFileHeader(); docInfo = new HwpDocInfo(this); bodyText = new ArrayList(); viewText = new ArrayList(); } public List getSections() { if (fileHeader.bDistributable) { return viewText; } else { return bodyText; } } public CompoundFile getOleFile() { return oleFile; } public boolean detect() throws HwpDetectException, IOException { // read CompoundFile structure try { oleFile.open(); if (getFileHeader() == false) { oleFile.close(); throw new HwpDetectException(ErrCode.FILE_READ_ERROR); } } catch (CompoundDetectException e) { oleFile.close(); throw new HwpDetectException(e.getReason()); } catch (HwpDetectException e) { oleFile.close(); throw new HwpDetectException(e.getReason()); } log.fine("Header parsed"); return true; } public void open() throws HwpDetectException, CompoundDetectException, IOException, DataFormatException, HwpParseException, NotImplementedException, CompoundParseException { if (fileHeader.signature==null || fileHeader.version==null) { detect(); } version = Integer.parseInt(fileHeader.version); if (fileHeader.bPasswordEncrypted) { throw new HwpParseException(); } if (getDocInfo(version)==false) throw new CompoundParseException(); log.fine("DocInfo parsed"); // 배포용 문서가 아니면 BodyText를 읽는다. if (fileHeader.bDistributable==false) { if (getBodyText(version)==false) throw new CompoundParseException(); log.fine("BodyText parsed"); } // 배포용 문서면 ViewText를 읽는다. if (fileHeader.bDistributable) { if (getViewText(version)==false) throw new CompoundParseException(); log.fine("Distributable file. ViewText parsed"); } } public void saveHwpComponent() throws IOException { Compressed compressed = fileHeader.bCompressed?Compressed.COMPRESS:Compressed.NO_COMPRESS; // Save internal component for debugging purpose. String patternStr = ".*"+Pattern.quote(File.separator)+"([^"+Pattern.quote(File.separator)+"]+)$"; String shortFilename = filename.replaceAll(patternStr, "$1"); // ".*\\\\([^\\\\]+)$" shortFilename = shortFilename.replaceAll("(.*)\\.hwp$", "$1"); log.finer("HwpFilePath="+filename + ", FileName="+shortFilename); File rootFolder = new File(shortFilename); if (!rootFolder.exists()) { Files.createDirectories(Paths.get(shortFilename)); } saveChildEntries(Paths.get(shortFilename), "Root Entry", compressed); } private void saveChildEntries(Path basePath, String storageName, Compressed compressed) throws IOException { List entries = oleFile.getChildEntries(storageName); for (DirectoryEntry entry: entries) { if (entry.getObjectType()==0x01) { Path childPath = Paths.get(basePath.toString(), entry.getDirectoryEntryName().trim()); if (childPath.toFile().exists()==false) { Files.createDirectory(childPath); } saveChildEntries(childPath, entry.getDirectoryEntryName().trim(), compressed); } else { byte[] buf = oleFile.read(entry); try (FileOutputStream fos = new FileOutputStream(Paths.get(basePath.toString(), entry.getDirectoryEntryName().trim()).toFile())) { if (compressed == Compressed.COMPRESS || (compressed==Compressed.FOLLOW_STORAGE && fileHeader.bCompressed)) { fos.write(unzip(oleFile.read(entry))); } else { fos.write(oleFile.read(entry)); } } catch (DataFormatException e) { e.printStackTrace(); } } } } private DirectoryEntry searchChildEntry(Path basePath, DirectoryEntry baseEntry, String entryName) throws IOException { List entries = oleFile.getChildEntries(baseEntry); for (DirectoryEntry entry: entries) { if (entry.getObjectType()==0x01) { Path childPath = Paths.get(basePath.toString(), entry.getDirectoryEntryName().trim()); // Files.createDirectory(childPath); return searchChildEntry(childPath, entry, entryName); } else { if (entry.getDirectoryEntryName().trim().equals(entryName)) { return entry; } } } return null; } public String saveChildEntry(Path rootPath, String entryName, Compressed compressed) throws IOException { File outputFile = null; String patternStr = ".*"+Pattern.quote(File.separator)+"([^"+Pattern.quote(File.separator)+"]+)$"; String shortFilename = filename.replaceAll(patternStr, "$1"); // ".*\\\\([^\\\\]+)$" shortFilename = shortFilename.replaceAll("(.*)\\.hwp$", "$1"); log.finer("HwpFilePath="+filename + ", FileName="+shortFilename); File rootFolder = new File(rootPath.toFile(), shortFilename); Path basePath; if (!rootFolder.exists()) { basePath = Files.createDirectories(Paths.get(rootPath.toString(), shortFilename)); } else { basePath = Paths.get(rootPath.toString(), shortFilename); } DirectoryEntry targetEntry = null; List entries = oleFile.getChildEntries("Root Entry"); for (DirectoryEntry entry: entries) { if (entry.getObjectType()==0x01) { Path childPath = Paths.get(rootPath.toString(), shortFilename, entry.getDirectoryEntryName().trim()); basePath = childPath; if (!childPath.toFile().exists()) { Files.createDirectory(childPath); } targetEntry = searchChildEntry(childPath, entry, entryName); if (targetEntry!=null) break; } else { if (entry.getDirectoryEntryName().trim().equals(entryName)) { targetEntry = entry; break; } } } if (targetEntry != null) { outputFile = Paths.get(basePath.toString(), targetEntry.getDirectoryEntryName().trim()).toFile(); if (outputFile.exists()==false) { try (FileOutputStream fos = new FileOutputStream(outputFile)) { if (compressed == Compressed.COMPRESS || (compressed==Compressed.FOLLOW_STORAGE && fileHeader.bCompressed)) { fos.write(unzip(oleFile.read(targetEntry))); } else { fos.write(oleFile.read(targetEntry)); } } catch (DataFormatException e) { e.printStackTrace(); } } } return outputFile==null?null:outputFile.getAbsolutePath(); } public byte[] getChildBytes(String entryName, Compressed compressed) throws IOException { byte[] retBytes = null; String patternStr = ".*"+Pattern.quote(File.separator)+"([^"+Pattern.quote(File.separator)+"]+)$"; String shortFilename = filename.replaceAll(patternStr, "$1"); // ".*\\\\([^\\\\]+)$" // 불필요코드이나 현재는 유지 shortFilename = shortFilename.replaceAll("(.*)\\.hwp$", "$1"); // 불필요코드이나 현재는 유지 log.finer("HwpFilePath="+filename + ", FileName="+shortFilename); DirectoryEntry targetEntry = null; List entries = oleFile.getChildEntries("Root Entry"); for (DirectoryEntry entry: entries) { if (entry.getObjectType()==0x01) { Path childPath = Paths.get(shortFilename, entry.getDirectoryEntryName().trim()); // 불필요코드이나 현재는 유지. targetEntry = searchChildEntry(childPath, entry, entryName); if (targetEntry!=null) break; } else { if (entry.getDirectoryEntryName().trim().equals(entryName)) { targetEntry = entry; break; } } } if (targetEntry != null) { if (compressed == Compressed.COMPRESS || (compressed==Compressed.FOLLOW_STORAGE && fileHeader.bCompressed)) { try { retBytes = unzip(oleFile.read(targetEntry)); } catch (IOException | DataFormatException e) { e.printStackTrace(); } } else { retBytes = oleFile.read(targetEntry); } } return retBytes; } private byte[] unzip(byte[] input) throws IOException, DataFormatException { Inflater decompressor = new Inflater(true); decompressor.setInput(input, 0, input.length); byte[] retBytes = null; try (ByteArrayOutputStream bos = new ByteArrayOutputStream(input.length)) { // Decompress the data byte[] buf = new byte[8096]; while (!decompressor.finished()) { int count = decompressor.inflate(buf); if (count > 0) { bos.write(buf, 0, count); } else { throw new IOException("can't decompress data"); } } bos.close(); retBytes = bos.toByteArray(); } return retBytes; } private byte[] decrypt(byte[] buf) throws HwpParseException { int offset = 0; int header = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0xFF0000 | buf[offset+1]<<8&0xFF00 | buf[offset]&0xFF; int tagNum = header&0x3FF; // 10 bits (0 - 9 bit) int level = (header&0xFFC00)>>>10; // 10 bits (10-19 bit) int size = (header&0xFFF00000)>>>20; // 12 bits (20-31 bit) offset += 4; HwpTag tag = HwpTag.from(tagNum); if (tag!=HwpTag.HWPTAG_DISTRIBUTE_DOC_DATA) { throw new HwpParseException(); } if (size != 256) { throw new HwpParseException(); } byte[] docData = new byte[256]; System.arraycopy(buf, offset, docData, 0, 256); offset += 256; int seed = docData[3]<<24&0xFF000000 | docData[2]<<16&0xFF0000 | docData[1]<<8&0xFF00 | docData[0]&0xFF; int hashoffset = (seed & 0x0f)+4; // 한글문서파일형식_배포용문서_revision1.2.hwp // MS Visual C의 랜덤함수 srand(), rand()를 사용 // 1. srand() 초기화 (Seed 사용) // 2. rand()함수의 결과 값을 이용하여 배열을 채운다. 단, rand()함수가 호출되는 순번에 따라 그 사용 방식이 달라진다.홀수번째 : 배열에 채워지는 값짝수번째 : 배열에 채워지는 횟수 // 3. 홀수번째 rand() & 0xFF의 값을 A라 하고, 짝수번째 (rand() & 0x0F + 1)의 결과를 B라 할 때배열에 A값을 B번 횟수만큼 삽입한다.예를 들어 A가 ‘a’이고, B가 3일 경우에 배열에 ‘a’를 3번 삽입한다. // 4. 배열크기가 256이 될 때까지 3항을 반복한다. Rand.srand(seed); for (int i=0; i < 256; ) { byte a = (byte) (Rand.rand() & 0x000000FF); int cnt = (Rand.rand() & 0x0000000F) + 1; for (int j=0; j sections = oleFile.getChildEntries("BodyText"); log.fine("BodyText has " + sections.size() + " children"); for (DirectoryEntry section: sections) { HwpSection hwpSection = new HwpSection(this); if (fileHeader.bCompressed) { hwpSection.parse(unzip(oleFile.read(section)), version); } else { hwpSection.parse(oleFile.read(section), version); } bodyText.add(hwpSection); } return true; } private boolean getViewText(int version) throws HwpParseException, NotImplementedException, IOException, DataFormatException { List sections = oleFile.getChildEntries("ViewText"); log.fine("ViewText has " + sections.size() + " children"); for (DirectoryEntry section: sections) { HwpSection hwpSection = new HwpSection(this); if (fileHeader.bCompressed) { hwpSection.parse(unzip(decrypt(oleFile.read(section))), version); } else { hwpSection.parse(decrypt(oleFile.read(section)), version); } viewText.add(hwpSection); } return true; } public byte[] getComponent(String entryName) throws CompoundDetectException { return oleFile.getComponent(entryName); } public void close() throws IOException { oleFile.close(); } public List getBinData() { return directoryBinData; } public void setBinData(List binData) { this.directoryBinData = binData; } public List getParaList() { return paraList; } public void addParaList(HwpParagraph para) { if (this.paraList == null) this.paraList = new ArrayList(); this.paraList.add(para); } public static class Rand { static int random_seed; public static void srand(int seed) { random_seed = seed; } public static int rand() { random_seed = (random_seed * 214013 + 2531011) & 0xFFFFFFFF; return ((random_seed >> 16) & 0x7FFF); } } } H2Orestart-0.7.2/source/HwpDoc/HwpFileHeader.java000066400000000000000000000133671476273367000215440ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc; import java.nio.charset.StandardCharsets; import org.w3c.dom.Document; import org.w3c.dom.Element; public class HwpFileHeader { public String signature; public String version; public boolean bCompressed; // 압축여부 boolean bPasswordEncrypted; // 암호설정여부 public boolean bDistributable; // 배포용 문서 여부 boolean bSaveScript; // 스크립트 저장 여부 boolean bDRMprotected; // DRM 보안 문서 여부 boolean bHasXMLTemplateStorage; // XMLTemplate 스토리지 존재 여부 boolean bHasDocumentHistory; // 문서 이력 관리 존재 여부 boolean bHasPkiSignature; // 전자 서명 정보 존재 여부 boolean bPkiEncrypted; // 공인 인증서 암호화 여부 boolean bReservePkiSignature; // 전자 서명 예비 저장 여부 boolean bPkiCertificateDRM; // 공인 인증서 DRM 보안 문서 여부 boolean bCCLDocument; // CCL 문서 여부 boolean bMobileOptimized; // 모바일 최적화 여부 boolean bPrivateInformation; // 개인 정보 보안 문서 여부 boolean bModifyTracking; // 변경 추적 문서 여부 boolean bCopyrightKOGL; // 공공누리(KOGL) 저작권 문서 boolean bHasVideoControl; // 비디오 컨트롤 포함 여부 boolean bHasMarkFieldControl; // 차례 필드 컨트롤 포함 여부 boolean bCopyrighted; // CCL, 공공누리 라이선스 정보 boolean bCopyProhibited; // 복제 제한 여부 boolean bCopyPermitted; // 동일 조건 하에 복제 허가 여부 int encryptVersion; int countryKOGLLicensed; // 공공누리(KOGL) 라이선스 지원 국가 public HwpFileHeader() { } // Compound형식 (hwp) boolean parse(byte[] buf) throws HwpDetectException { int offset = 0; signature = new String(buf, offset, 32, StandardCharsets.US_ASCII); if (signature.trim().equals("HWP Document File")==false) { throw new HwpDetectException(ErrCode.SIGANTURE_NOT_MATCH); } offset += 32; version = new Integer(buf[offset+3]).toString() + new Integer(buf[offset+2]).toString() + new Integer(buf[offset+1]).toString() + new Integer(buf[offset+0]).toString(); offset += 4; bCompressed = (buf[offset]&0x01)==0x01?true:false; bPasswordEncrypted = (buf[offset]&0x02)==0x02?true:false; bDistributable = (buf[offset]&0x04)==0x04?true:false; bSaveScript = (buf[offset]&0x08)==0x08?true:false; bDRMprotected = (buf[offset]&0x10)==0x10?true:false; bHasXMLTemplateStorage = (buf[offset]&0x20)==0x20?true:false; bHasDocumentHistory = (buf[offset]&0x40)==0x40?true:false; bHasPkiSignature = (buf[offset]&0x80)==0x80?true:false; bPkiEncrypted = (buf[offset+1]&0x01)==0x01?true:false; bReservePkiSignature = (buf[offset+1]&0x02)==0x02?true:false; // 전자 서명 예비 저장 여부 bPkiCertificateDRM = (buf[offset+1]&0x04)==0x04?true:false; bCCLDocument = (buf[offset+1]&0x08)==0x08?true:false; bMobileOptimized = (buf[offset+1]&0x10)==0x10?true:false; bPrivateInformation = (buf[offset+1]&0x20)==0x20?true:false; bModifyTracking = (buf[offset+1]&0x40)==0x40?true:false; bCopyrightKOGL = (buf[offset+1]&0x80)==0x80?true:false; bHasVideoControl = (buf[offset+2]&0x01)==0x01?true:false; bHasMarkFieldControl = (buf[offset+2]&0x02)==0x02?true:false; offset += 4; bCopyrighted = (buf[offset]&0x01)==0x01?true:false; bCopyProhibited = (buf[offset]&0x02)==0x02?true:false; bCopyPermitted = (buf[offset]&0x04)==0x04?true:false; offset += 4; encryptVersion = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0xFF0000 | buf[offset+1]<<8&0xFF00 | buf[offset]&0xFF; offset += 4; countryKOGLLicensed = (int)buf[offset]; return true; } // Owpml형식 (hwpx) boolean parse(Document document) { Element element = document.getDocumentElement(); String docVersion = element.getAttribute("major"); docVersion += element.getAttribute("minor"); docVersion += element.getAttribute("micro"); docVersion += element.getAttribute("buildNumber"); version = docVersion; /* NodeList nodeList = element.getChildNodes(); for (int i = 0; i < nodeList.getLength(); i++) { Node node = nodeList.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { Element elem = (Element) node; String firstname = elem.getElementsByTagName("firstname") .item(0).getChildNodes().item(0).getNodeValue(); String lastname = elem.getElementsByTagName("lastname").item(0) .getChildNodes().item(0).getNodeValue(); Double salary = Double.parseDouble(elem.getElementsByTagName("salary") .item(0).getChildNodes().item(0).getNodeValue()); } } */ return true; } } H2Orestart-0.7.2/source/HwpDoc/HwpSection.java000066400000000000000000001545111476273367000211550ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Optional; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; import HwpDoc.HwpElement.HwpRecord_CtrlData; import HwpDoc.HwpElement.HwpRecord_CtrlHeader; import HwpDoc.HwpElement.HwpRecord_FormObject; import HwpDoc.HwpElement.HwpRecord_ListHeader; import HwpDoc.HwpElement.HwpRecord_ParaRangeTag; import HwpDoc.HwpElement.HwpRecord_ParaText; import HwpDoc.HwpElement.HwpTag; import HwpDoc.paragraph.CapParagraph; import HwpDoc.paragraph.CellParagraph; import HwpDoc.paragraph.CharShape; import HwpDoc.paragraph.Ctrl; import HwpDoc.paragraph.Ctrl_Character; import HwpDoc.paragraph.Ctrl_Common; import HwpDoc.paragraph.Ctrl_Common.VertAlign; import HwpDoc.paragraph.Ctrl_Container; import HwpDoc.paragraph.Ctrl_EqEdit; import HwpDoc.paragraph.Ctrl_Form; import HwpDoc.paragraph.Ctrl_GeneralShape; import HwpDoc.paragraph.Ctrl_HeadFoot; import HwpDoc.paragraph.Ctrl_Note; import HwpDoc.paragraph.Ctrl_SectionDef; import HwpDoc.paragraph.Ctrl_ShapeArc; import HwpDoc.paragraph.Ctrl_ShapeCurve; import HwpDoc.paragraph.Ctrl_ShapeEllipse; import HwpDoc.paragraph.Ctrl_ShapeLine; import HwpDoc.paragraph.Ctrl_ShapeOle; import HwpDoc.paragraph.Ctrl_ShapePic; import HwpDoc.paragraph.Ctrl_ShapePolygon; import HwpDoc.paragraph.Ctrl_ShapeRect; import HwpDoc.paragraph.Ctrl_ShapeTextArt; import HwpDoc.paragraph.Ctrl_ShapeVideo; import HwpDoc.paragraph.Ctrl_Table; import HwpDoc.paragraph.HwpParagraph; import HwpDoc.paragraph.LineSeg; import HwpDoc.paragraph.TblCell; import HwpDoc.paragraph.Ctrl_Character.CtrlCharType; import HwpDoc.section.NoteShape; import HwpDoc.section.Page; import HwpDoc.section.PageBorderFill; public class HwpSection { private static final Logger log = Logger.getLogger(HwpSection.class.getName()); public List paraList; public HwpSection(HwpFile hwp) { paraList = new ArrayList(); } public HwpSection(HwpxFile hwpx) { paraList = new ArrayList(); } boolean read(Document document, int version) throws NotImplementedException { Element element = document.getDocumentElement(); paraList = new ArrayList(); NodeList nodeList = element.getChildNodes(); for (int i = 0; i < nodeList.getLength(); i++) { Node node = nodeList.item(i); HwpParagraph para = null; switch(node.getNodeName()) { case "hp:p": para = new HwpParagraph(node, version); paraList.add(para); break; } } return true; } boolean parse(byte[] buf, int version) throws HwpParseException { int off = 0; while(off < buf.length) { int header = buf[off+3]<<24&0xFF000000 | buf[off+2]<<16&0xFF0000 | buf[off+1]<<8&0xFF00 | buf[off]&0xFF; int tagNum = header&0x3FF; // 10 bits (0 - 9 bit) int level = (header&0xFFC00)>>>10; // 10 bits (10-19 bit) int size = (header&0xFFF00000)>>>20; // 12 bits (20-31 bit) if (level>0) { HwpParagraph para = paraList.stream().reduce((a,b)->b).get(); off += parseRecurse(para, level, buf, off, version); } else { if (size==0xFFF) { size = buf[off+7]<<24&0xFF000000 | buf[off+6]<<16&0xFF0000 | buf[off+5]<<8&0xFF00 | buf[off+4]&0xFF; off += 8; } else { off += 4; } HwpTag tag = HwpTag.from(tagNum); log.fine(IntStream.rangeClosed(0, level).mapToObj(i -> String.valueOf(i)).collect(Collectors.joining())+"[TAG]="+tag.toString()+" ("+size+")"); if (level==0 && tag==HwpTag.HWPTAG_PARA_HEADER) { HwpParagraph currPara = HwpParagraph.parse(tagNum, level, size, buf, off, version); paraList.add(currPara); off += size; } } } return true; } private int parseRecurse(HwpParagraph currPara, int runLevel, byte[] buf, int off, int version) throws HwpParseException { int offset = off; while(offset < buf.length) { int header = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0xFF0000 | buf[offset+1]<<8&0xFF00 | buf[offset]&0xFF; int tagNum = header&0x3FF; // 10 bits (0 - 9 bit) int level = (header&0xFFC00)>>>10; // 10 bits (10-19 bit) int size = (header&0xFFF00000)>>>20; // 12 bits (20-31 bit) int headerOffset = 0; if (size==0xFFF) { size = buf[offset+7]<<24&0xFF000000 | buf[offset+6]<<16&0xFF0000 | buf[offset+5]<<8&0xFF00 | buf[offset+4]&0xFF; headerOffset = 8; } else { headerOffset = 4; } if (level < runLevel) { break; } HwpTag tag = HwpTag.from(tagNum); if (level > runLevel) { log.fine(IntStream.rangeClosed(0, level).mapToObj(i -> String.valueOf(i)).collect(Collectors.joining())+"[TAG]="+tag.toString()+" > runLevel"); switch(tag) { case HWPTAG_PARA_HEADER: case HWPTAG_PARA_TEXT: case HWPTAG_PARA_CHAR_SHAPE: case HWPTAG_PARA_LINE_SEG: case HWPTAG_PARA_RANGE_TAG: case HWPTAG_CTRL_HEADER: offset += parseRecurse(currPara, level, buf, offset, version); break; case HWPTAG_TABLE: { Ctrl_Table table = (Ctrl_Table)currPara.p.stream().filter(c -> (c instanceof Ctrl_Table)).reduce((a,b)->b).get(); offset += parseCtrlRecurse(table, level, buf, offset, version); } break; case HWPTAG_LIST_HEADER: offset += headerOffset; offset += size; break; case HWPTAG_PAGE_DEF: case HWPTAG_FOOTNOTE_SHAPE: case HWPTAG_PAGE_BORDER_FILL: { // dces 컨트롤에서만 처리 Ctrl_SectionDef ctrlSecd = (Ctrl_SectionDef)currPara.p.stream().filter(c -> (c.ctrlId.equals("dces"))).reduce((a,b)->b).get(); offset += parseCtrlRecurse(ctrlSecd, level, buf, offset, version); } break; case HWPTAG_SHAPE_COMPONENT: case HWPTAG_SHAPE_COMPONENT_PICTURE: case HWPTAG_SHAPE_COMPONENT_LINE: case HWPTAG_SHAPE_COMPONENT_RECTANGLE: case HWPTAG_SHAPE_COMPONENT_ELLIPSE: case HWPTAG_SHAPE_COMPONENT_ARC: case HWPTAG_SHAPE_COMPONENT_POLYGON: case HWPTAG_SHAPE_COMPONENT_CURVE: case HWPTAG_SHAPE_COMPONENT_OLE: case HWPTAG_EQEDIT: case HWPTAG_SHAPE_COMPONENT_TEXTART: case HWPTAG_SHAPE_COMPONENT_UNKNOWN: { // " osg" 컨트롤에서만 처리 Ctrl_GeneralShape ctrlGeneral = (Ctrl_GeneralShape)currPara.p.stream().filter(c -> (c.ctrlId.equals(" osg"))).reduce((a,b)->b).get(); offset += parseCtrlRecurse(ctrlGeneral, level, buf, offset, version); } break; case HWPTAG_CTRL_DATA: case HWPTAG_FORM_OBJECT: case HWPTAG_MEMO_SHAPE: case HWPTAG_MEMO_LIST: case HWPTAG_CHART_DATA: case HWPTAG_VIDEO_DATA: default: { // 마지막 컨트롤을 기준으로 parseRecurse Ctrl_Common ctrlCommon = (Ctrl_Common) currPara.p.stream().filter(c -> (c instanceof Ctrl_Common)).reduce((a,b)->b).get(); offset += parseCtrlRecurse(ctrlCommon, level, buf, offset, version); } } } else if (level==runLevel) { offset += headerOffset; log.fine(IntStream.rangeClosed(0, level).mapToObj(i -> String.valueOf(i)).collect(Collectors.joining())+"[TAG]="+tag.toString()+" ("+size+") = runLevel"); switch(tag) { case HWPTAG_PARA_HEADER: if (currPara instanceof CellParagraph) { return offset-headerOffset-off; // CELL에 여러개의 PARA가 들어가기 위해 필요. } else if (currPara instanceof CapParagraph) { offset += HwpParagraph.parse(currPara, size, buf, offset, version); // 캡션을 읽기 위해 필요. break; } else { if (runLevel==1) { // runLevel=1에서 HWPTAG_PARA_HEADER 예상하지 못함. PARA_HEADER 뒤에 PARA_HEADER 인 경우, return offset-off + size;// runLevel=1일 경우, PARA_HEADER skip하도록 함. } return offset-headerOffset-off; // CELL에 여러개의 PARA가 들어가기 위해 필요. } case HWPTAG_PARA_TEXT: { if (currPara.p==null) currPara.p = new LinkedList<>(); currPara.p.addAll(HwpRecord_ParaText.parse(tagNum, level, size, buf, offset, version)); // paraText를 LinkedList로 변경하자 offset += size; } break; case HWPTAG_PARA_CHAR_SHAPE: { if (currPara.p == null) { currPara.p = new LinkedList<>(); currPara.p.add(new Ctrl_Character(" _", CtrlCharType.PARAGRAPH_BREAK)); } CharShape.fillCharShape(tagNum, level, size, buf, offset, version, currPara.p); offset += size; } break; case HWPTAG_PARA_LINE_SEG: currPara.lineSegs = new LineSeg(tagNum, level, size, buf, offset, version); offset += size; break; case HWPTAG_PARA_RANGE_TAG: HwpRecord_ParaRangeTag.parse(currPara, tagNum, level, size, buf, offset, version); offset += size; break; case HWPTAG_CTRL_HEADER: { Ctrl ctrl = HwpRecord_CtrlHeader.parse(tagNum, level, size, buf, offset, version); if (ctrl instanceof Ctrl_GeneralShape) { ((Ctrl_GeneralShape) ctrl).setParent(currPara); } Optional ctrlOrigOp = currPara.p.stream().filter(c -> (c.ctrlId.equals(ctrl.ctrlId))) .filter(c -> c.fullfilled==false) .findFirst(); if (ctrlOrigOp.isPresent()) { int linkedListIdx = currPara.p.indexOf(ctrlOrigOp.get()); currPara.p.set(linkedListIdx, ctrl); } if (ctrl instanceof Ctrl_HeadFoot) { Optional secd2Op = currPara.p.stream().filter(c -> (c.ctrlId.equals("dces"))).reduce((a,b)->b); if (secd2Op.isPresent()) { Ctrl_SectionDef secd = (Ctrl_SectionDef) secd2Op.get(); if (secd.headerFooter==null) secd.headerFooter = new ArrayList(); secd.headerFooter.add((Ctrl_HeadFoot)ctrl); } } // HWPTAG_LIST_HEADER를 통해 캡션을 얻어오기 위해 조치. parseCtrlRecurs가 없으면 ParseParaRecurs에서 무한루프. if (ctrl instanceof Ctrl_Table) { offset += size; offset += parseCtrlRecurse(ctrl, level, buf, offset, version); } else { offset += size; offset += parseCtrlRecurse(ctrl, level, buf, offset, version); } // HWPTAG_LIST_HEADER를 통해 캡션을 얻어오기 위해 조치. parseCtrlRecurs가 없으면 ParseParaRecurs에서 무한루프. } break; case HWPTAG_TABLE: return offset-headerOffset-off; case HWPTAG_LIST_HEADER: if (runLevel==1) { // runLevel=1에서 LIST_HEADER 예상하지 못함. CTRL이 아닌 PARA_HEADER 다음에 PARA_HEADER return offset-off + size; // runLevel=1일 경우, LIST_HEADER skip하도록 함. } return offset-headerOffset-off; case HWPTAG_SHAPE_COMPONENT: case HWPTAG_SHAPE_COMPONENT_PICTURE: case HWPTAG_SHAPE_COMPONENT_LINE: case HWPTAG_SHAPE_COMPONENT_RECTANGLE: case HWPTAG_SHAPE_COMPONENT_ELLIPSE: case HWPTAG_SHAPE_COMPONENT_ARC: case HWPTAG_SHAPE_COMPONENT_POLYGON: case HWPTAG_SHAPE_COMPONENT_CURVE: case HWPTAG_SHAPE_COMPONENT_OLE: case HWPTAG_EQEDIT: case HWPTAG_SHAPE_COMPONENT_TEXTART: return offset-headerOffset-off; case HWPTAG_CTRL_DATA: case HWPTAG_FORM_OBJECT: case HWPTAG_MEMO_SHAPE: case HWPTAG_MEMO_LIST: case HWPTAG_CHART_DATA: case HWPTAG_VIDEO_DATA: case HWPTAG_SHAPE_COMPONENT_UNKNOWN: offset += size; break; default: } } } return offset-off; } int parseCtrlRecurse(Ctrl currCtrl, int runLevel, byte[] buf, int off, int version) throws HwpParseException { int offset = off; Ctrl ctrl = currCtrl; while (offset < buf.length) { int header = buf[offset + 3] << 24 & 0xFF000000 | buf[offset + 2] << 16 & 0xFF0000 | buf[offset + 1] << 8 & 0xFF00 | buf[offset] & 0xFF; int tagNum = header & 0x3FF; // 10 bits (0 - 9 bit) int level = (header & 0xFFC00) >>> 10; // 10 bits (10-19 bit) int size = (header & 0xFFF00000) >>> 20; // 12 bits (20-31 bit) int headerOffset = 0; if (size == 0xFFF) { size = buf[off + 7] << 24 & 0xFF000000 | buf[off + 6] << 16 & 0xFF0000 | buf[off + 5] << 8 & 0xFF00 | buf[off + 4] & 0xFF; headerOffset = 8; } else { headerOffset = 4; } if (level < runLevel) { break; } HwpTag tag = HwpTag.from(tagNum); if (level > runLevel) { log.fine(IntStream.rangeClosed(0, level).mapToObj(i -> String.valueOf(i)).collect(Collectors.joining()) + "[TAG]=" + tag.toString() + " (" + size + ") > runLevel"); switch (tag) { case HWPTAG_PARA_HEADER: if (ctrl instanceof Ctrl_Common) { offset += parseCtrlRecurse((Ctrl_Common) ctrl, level, buf, offset, version); } else { offset += headerOffset; log.severe("Unknown CtrlId=" + ctrl.ctrlId + ". Skip HWPTAG_PARA_HEADER"); offset += size; } break; case HWPTAG_PARA_TEXT: if (ctrl instanceof Ctrl_Common) { HwpParagraph lastPara = ((Ctrl_Common) ctrl).paras.get(((Ctrl_Common) ctrl).paras.size() - 1); offset += parseRecurse(lastPara, level, buf, offset, version); } else { offset += headerOffset; log.severe("Unknown CtrlId=" + ctrl.ctrlId + ". Skip HWPTAG_PARA_HEADER"); offset += size; } break; case HWPTAG_LIST_HEADER: // LIST_HEADER만 recursive하게 처리하지 않고, 여기서 처리한다. offset += headerOffset; int subParaCount = HwpRecord_ListHeader.getCount(tagNum, level, size, buf, offset, version); offset += 6; // 문단수 2byte, 속성 4byte if (ctrl instanceof Ctrl_Table) { int len = parseListAppend((Ctrl_Common) ctrl, size - 6, buf, offset, version); offset += len; // 캡션이냐, Cell이냐 무엇으로 해석할지 판단. if (((Ctrl_Table) ctrl).cells != null) { // HWPTAG_TABLE을 지났을때는 Cell로 해석하자. } else { // HWPTAG_TABLE을 거치지 않았을때는 캡션으로 해석. if (((Ctrl_Table) ctrl).caption == null) ((Ctrl_Table) ctrl).caption = new ArrayList(); CapParagraph newPara = new CapParagraph(); ((Ctrl_Table) ctrl).caption.add(newPara); offset += parseRecurse(newPara, level, buf, offset, version); } } else if (ctrl instanceof Ctrl_ShapeRect) { Ctrl_Common ctrlCmn = (Ctrl_Common) ctrl; offset -= 6; ctrlCmn.textVerAlign = HwpRecord_ListHeader.getVertAlign(6, buf, offset, version); offset += 6; offset += parseListAppend(ctrlCmn, size - 6, buf, offset, version); offset += parseCtrlRecurse(ctrl, level, buf, offset, version); } else if (ctrl instanceof Ctrl_GeneralShape) { // [그림의 caption 을 넣기 위한 임시 코드] // Ctrl_Common ctrlCmn = (Ctrl_Common)ctrl; // offset -= 6; // ctrlCmn.textVerAlign = HwpRecord_ListHeader.getVertAlign(6, buf, offset, // version); // offset += 6; // offset += parseListAppend(ctrlCmn, size-6, buf, offset, version); // if (subParaCount>0) { // if (ctrlCmn.caption==null) ctrlCmn.caption = new ArrayList(); // CapParagraph newPara = new CapParagraph(); // ctrlCmn.caption.add(newPara); // offset += parseRecurse(newPara, level, buf, offset, version); // } // [그림의 caption 을 넣기 위한 임시 코드] // 글상자속성 Ctrl_Common ctrlCmn = (Ctrl_Common) ctrl; offset -= 6; ctrlCmn.textVerAlign = HwpRecord_ListHeader.getVertAlign(6, buf, offset, version); offset += 6; offset += parseListAppend(ctrlCmn, size - 6, buf, offset, version); offset += parseCtrlRecurse(ctrl, level, buf, offset, version); // 글상자속성 } else if (ctrl instanceof Ctrl_HeadFoot) { int len = parseListAppend(ctrl, size - 6, buf, offset, version); offset += len; offset += parseCtrlRecurse(ctrl, level, buf, offset, version); } else if (ctrl instanceof Ctrl_Note) { int len = parseListAppend(ctrl, size - 6, buf, offset, version); offset += len; offset += parseCtrlRecurse(ctrl, level, buf, offset, version); } else { offset += (size - 6); } break; case HWPTAG_PAGE_DEF: case HWPTAG_FOOTNOTE_SHAPE: case HWPTAG_PAGE_BORDER_FILL: if (ctrl instanceof Ctrl_SectionDef) { // dces 컨트롤에서만 처리 offset += parseCtrlRecurse((Ctrl_SectionDef) ctrl, level, buf, offset, version); } break; case HWPTAG_SHAPE_COMPONENT: case HWPTAG_SHAPE_COMPONENT_PICTURE: case HWPTAG_SHAPE_COMPONENT_LINE: case HWPTAG_SHAPE_COMPONENT_RECTANGLE: case HWPTAG_SHAPE_COMPONENT_ELLIPSE: case HWPTAG_SHAPE_COMPONENT_ARC: case HWPTAG_SHAPE_COMPONENT_POLYGON: case HWPTAG_SHAPE_COMPONENT_CURVE: case HWPTAG_SHAPE_COMPONENT_OLE: case HWPTAG_EQEDIT: case HWPTAG_SHAPE_COMPONENT_TEXTART: case HWPTAG_SHAPE_COMPONENT_UNKNOWN: if (ctrl instanceof Ctrl_GeneralShape) { // " osg" 컨트롤에서만 처리 offset += parseCtrlRecurse((Ctrl_GeneralShape) ctrl, level, buf, offset, version); } else { return offset - off; } break; case HWPTAG_TABLE: { // 마지막 컨트롤을 기준으로 parseRecurse offset += parseCtrlRecurse(ctrl, level, buf, offset, version); } break; case HWPTAG_CTRL_HEADER: // [2024.05.27] return offset - off; case HWPTAG_PARA_RANGE_TAG: case HWPTAG_CTRL_DATA: case HWPTAG_FORM_OBJECT: case HWPTAG_MEMO_SHAPE: case HWPTAG_MEMO_LIST: case HWPTAG_CHART_DATA: case HWPTAG_VIDEO_DATA: default: { // 마지막 컨트롤을 기준으로 parseRecurse offset += parseCtrlRecurse(ctrl, level, buf, offset, version); } } } else if (level == runLevel) { offset += headerOffset; log.fine(IntStream.rangeClosed(0, level).mapToObj(i -> String.valueOf(i)).collect(Collectors.joining()) + "[TAG]=" + tag.toString() + " (" + size + ") = runLevel"); switch (tag) { case HWPTAG_PARA_HEADER: if (ctrl instanceof Ctrl_Table) { if (((Ctrl_Table) ctrl).cells == null) { // 캡션. if (((Ctrl_Table) ctrl).paras == null) ((Ctrl_Table) ctrl).paras = new ArrayList(); HwpParagraph newPara = HwpParagraph.parse(tagNum, level, size, buf, offset, version); // HwpParagraph newPara = new HwpParagraph(); ((Ctrl_Table) ctrl).paras.add(newPara); offset += size; // HWP_PARA_HEADER의 하위 level부터 읽도록 offset 변경. offset += parseRecurse(newPara, level, buf, offset, version); } else { // 마지막 cell 내 para list에 PARA 추가 TblCell cell = ((Ctrl_Table) ctrl).cells.stream().reduce((a, b) -> b).get(); if (cell.paras == null) cell.paras = new ArrayList(); CellParagraph newPara = new CellParagraph(); cell.paras.add(newPara); // Cell 내용에 대한 PARA_HEADER 읽어야지요. offset += HwpParagraph.parse(newPara, size, buf, offset, version); // HWP_PARA_HEADER의 하위 level부터 읽도록 offset 변경되었음. offset += parseRecurse(newPara, level, buf, offset, version); } } else if (ctrl instanceof Ctrl_ShapeRect) { if (((Ctrl_Common) ctrl).paras == null) ((Ctrl_Common) ctrl).paras = new ArrayList(); HwpParagraph newPara = HwpParagraph.parse(tagNum, level, size, buf, offset, version); ((Ctrl_Common) ctrl).paras.add(newPara); offset += HwpParagraph.parse(newPara, size, buf, offset, version); // parseRecurse에서 PARA_TEXT 부터 읽도록 offset 변경되었음. offset += parseRecurse(newPara, level, buf, offset, version); } else if (ctrl instanceof Ctrl_GeneralShape) { if (((Ctrl_Common) ctrl).captionWidth > 0 && ((Ctrl_Common) ctrl).caption == null) { ((Ctrl_Common) ctrl).caption = new ArrayList(); CapParagraph newPara = new CapParagraph(); // HwpRecord_ParaHeader.parse(tagNum, level, // size, buf, offset, version); ((Ctrl_Common) ctrl).caption.add(newPara); offset += HwpParagraph.parse(newPara, size, buf, offset, version); // parseRecurse에서 PARA_TEXT 부터 읽도록 offset 변경되었음. offset += parseRecurse(newPara, level, buf, offset, version); } else { if (((Ctrl_Common) ctrl).paras == null) ((Ctrl_Common) ctrl).paras = new ArrayList(); HwpParagraph newPara = HwpParagraph.parse(tagNum, level, size, buf, offset, version); ((Ctrl_Common) ctrl).paras.add(newPara); offset += HwpParagraph.parse(newPara, size, buf, offset, version); // parseRecurse에서 PARA_TEXT 부터 읽도록 offset 변경되었음. offset += parseRecurse(newPara, level, buf, offset, version); } } else if (ctrl instanceof Ctrl_HeadFoot) { if (((Ctrl_HeadFoot) ctrl).paras == null) ((Ctrl_HeadFoot) ctrl).paras = new ArrayList(); HwpParagraph newPara = HwpParagraph.parse(tagNum, level, size, buf, offset, version); ((Ctrl_HeadFoot) ctrl).paras.add(newPara); offset += size; // parseRecurse에서 PARA_TEXT 부터 읽도록 offset 변경. offset += parseRecurse(newPara, level, buf, offset, version); } else if (ctrl instanceof Ctrl_SectionDef) { // 바탕쪽 (Header/Footer와 유사) if (((Ctrl_SectionDef) ctrl).paras == null) ((Ctrl_SectionDef) ctrl).paras = new ArrayList(); HwpParagraph newPara = HwpParagraph.parse(tagNum, level, size, buf, offset, version); ((Ctrl_SectionDef) ctrl).paras.add(newPara); offset += size; offset += parseRecurse(newPara, level, buf, offset, version); } else if (ctrl instanceof Ctrl_Note) { if (((Ctrl_Note) ctrl).paras == null) ((Ctrl_Note) ctrl).paras = new ArrayList(); HwpParagraph newPara = HwpParagraph.parse(tagNum, level, size, buf, offset, version); ((Ctrl_Note) ctrl).paras.add(newPara); offset += size; offset += parseRecurse(newPara, level, buf, offset, version); } else { HwpParagraph newPara = HwpParagraph.parse(tagNum, level, size, buf, offset, version); offset += size; parseRecurse(newPara, level, buf, offset, version); } break; case HWPTAG_CTRL_HEADER: return offset - headerOffset - off; case HWPTAG_PAGE_DEF: if (ctrl instanceof Ctrl_SectionDef) { ((Ctrl_SectionDef) ctrl).page = Page.parse(level, size, buf, offset, version); } offset += size; break; case HWPTAG_FOOTNOTE_SHAPE: if (ctrl instanceof Ctrl_SectionDef) { Ctrl_SectionDef secCtrl = (Ctrl_SectionDef) ctrl; if (secCtrl.noteShapes == null) secCtrl.noteShapes = new ArrayList(); secCtrl.noteShapes.add(NoteShape.parse(level, size, buf, offset, version)); } offset += size; break; case HWPTAG_PAGE_BORDER_FILL: if (ctrl instanceof Ctrl_SectionDef) { Ctrl_SectionDef secCtrl = (Ctrl_SectionDef) ctrl; if (secCtrl.borderFills == null) secCtrl.borderFills = new ArrayList(); secCtrl.borderFills.add(PageBorderFill.parse(level, size, buf, offset, version)); } offset += size; break; case HWPTAG_TABLE: { // 이후에오는 {LIST_HEADER+...} 들을 cell로 받아야 한다. int len = Ctrl_Table.parseCtrl((Ctrl_Table) ctrl, size, buf, offset, version); } offset += size; break; case HWPTAG_LIST_HEADER: if (ctrl instanceof Ctrl_Table) { VertAlign verAlign = HwpRecord_ListHeader.getVertAlign(size, buf, offset, version); offset += 6; // 문단수 2byte, 속성 4byte if (((Ctrl_Table) ctrl).cells == null) ((Ctrl_Table) ctrl).cells = new ArrayList(); // LIST_HEADER에 붙어서 오는 41byte 먼저 읽고, TblCell cell = new TblCell(size - 6, buf, offset, version); cell.verAlign = verAlign; // 세로 정렬 offset += cell.getSize(); ((Ctrl_Table) ctrl).cells.add(cell); /* * 그림의 caption을 구하기 위해 임시 코드. } else if (ctrl instanceof Ctrl_Common) { * Ctrl_Common ctrlCmn = (Ctrl_Common)ctrl; offset += parseListAppend(ctrlCmn, * size-6, buf, offset, version); */ } else { offset += size; } break; case HWPTAG_SHAPE_COMPONENT: if (ctrl instanceof Ctrl_Container) { offset -= headerOffset; if (((Ctrl_Container) ctrl).list == null) ((Ctrl_Container) ctrl).list = new ArrayList(); offset += parseContainerRecurse((Ctrl_Container) ctrl, level, buf, offset, version); } else if (ctrl instanceof Ctrl_GeneralShape) { Ctrl_GeneralShape newCtrl = Ctrl_GeneralShape.parse((Ctrl_GeneralShape) ctrl, size, buf, offset, version); // replace Ctrl with newCtrl HwpParagraph parentPara = ((Ctrl_GeneralShape) ctrl).getParent(); int ctrlIndex = parentPara.p.indexOf(ctrl); if (ctrlIndex>=0) { parentPara.p.set(ctrlIndex, newCtrl); } else { parentPara.p.add(newCtrl); } ctrl = newCtrl; offset += size; } break; case HWPTAG_SHAPE_COMPONENT_PICTURE: if (ctrl instanceof Ctrl_ShapePic) { Ctrl_ShapePic.parseElement((Ctrl_ShapePic) ctrl, size, buf, offset, version); } offset += size; break; case HWPTAG_SHAPE_COMPONENT_LINE: if (ctrl instanceof Ctrl_ShapeLine) { Ctrl_ShapeLine.parseElement((Ctrl_ShapeLine) ctrl, size, buf, offset, version); } offset += size; break; case HWPTAG_SHAPE_COMPONENT_RECTANGLE: if (ctrl instanceof Ctrl_ShapeRect) { Ctrl_ShapeRect.parseElement((Ctrl_ShapeRect) ctrl, size, buf, offset, version); } offset += size; break; case HWPTAG_SHAPE_COMPONENT_ELLIPSE: if (ctrl instanceof Ctrl_ShapeEllipse) { Ctrl_ShapeEllipse.parseElement((Ctrl_ShapeEllipse) ctrl, size, buf, offset, version); } offset += size; break; case HWPTAG_SHAPE_COMPONENT_ARC: if (ctrl instanceof Ctrl_ShapeArc) { Ctrl_ShapeArc.parseElement((Ctrl_ShapeArc) ctrl, size, buf, offset, version); } offset += size; break; case HWPTAG_SHAPE_COMPONENT_POLYGON: if (ctrl instanceof Ctrl_ShapePolygon) { Ctrl_ShapePolygon.parseElement((Ctrl_ShapePolygon) ctrl, size, buf, offset, version); } offset += size; break; case HWPTAG_SHAPE_COMPONENT_CURVE: if (ctrl instanceof Ctrl_ShapeCurve) { Ctrl_ShapeCurve.parseElement((Ctrl_ShapeCurve) ctrl, size, buf, offset, version); } offset += size; break; case HWPTAG_SHAPE_COMPONENT_OLE: if (ctrl instanceof Ctrl_ShapeOle) { Ctrl_ShapeOle.parseElement((Ctrl_ShapeOle) ctrl, size, buf, offset, version); } offset += size; break; case HWPTAG_EQEDIT: if (ctrl instanceof Ctrl_EqEdit) { Ctrl_EqEdit.parseElement((Ctrl_EqEdit) ctrl, size, buf, offset, version); } offset += size; break; case HWPTAG_VIDEO_DATA: if (ctrl instanceof Ctrl_ShapeVideo) { Ctrl_ShapeVideo.parseElement((Ctrl_ShapeVideo) ctrl, size, buf, offset, version); } offset += size; break; case HWPTAG_SHAPE_COMPONENT_TEXTART: if (ctrl instanceof Ctrl_ShapeTextArt) { Ctrl_ShapeTextArt.parseElement((Ctrl_ShapeTextArt) ctrl, size, buf, offset, version); } offset += size; break; case HWPTAG_SHAPE_COMPONENT_UNKNOWN: offset += size; break; case HWPTAG_FORM_OBJECT: if (ctrl instanceof Ctrl_Form) { HwpRecord_FormObject.parseCtrl((Ctrl_Form) ctrl, size, buf, offset, version); } offset += size; break; case HWPTAG_CTRL_DATA: HwpRecord_CtrlData.parseCtrl(ctrl, size, buf, offset, version); offset += size; break; default: offset += size; } } } return offset - off; } int parseContainerRecurse(Ctrl_Container container, int runLevel, byte[] buf, int off, int version) throws HwpParseException { int offset = off; while (offset < buf.length) { int header = buf[offset + 3] << 24 & 0xFF000000 | buf[offset + 2] << 16 & 0xFF0000 | buf[offset + 1] << 8 & 0xFF00 | buf[offset] & 0xFF; int tagNum = header & 0x3FF; // 10 bits (0 - 9 bit) int level = (header & 0xFFC00) >>> 10; // 10 bits (10-19 bit) int size = (header & 0xFFF00000) >>> 20; // 12 bits (20-31 bit) int headerOffset = 0; if (size == 0xFFF) { size = buf[off + 7] << 24 & 0xFF000000 | buf[off + 6] << 16 & 0xFF0000 | buf[off + 5] << 8 & 0xFF00 | buf[off + 4] & 0xFF; headerOffset = 8; } else { headerOffset = 4; } if (level < runLevel) { break; } HwpTag tag = HwpTag.from(tagNum); if (level > runLevel) { offset += headerOffset; log.fine(IntStream.rangeClosed(0, level).mapToObj(i -> String.valueOf(i)).collect(Collectors.joining()) + "[TAG]=" + tag.toString() + " (" + size + ") > runLevel"); switch (tag) { case HWPTAG_SHAPE_COMPONENT_PICTURE: { Ctrl_GeneralShape ctrl = null; if (container.list != null) { Optional opCtrl = container.list.stream() .filter(c -> (c instanceof Ctrl_ShapePic)).reduce((a, b) -> b); if (opCtrl.isPresent()) { ctrl = opCtrl.get(); } } if (ctrl == null) { ctrl = new Ctrl_ShapePic(new Ctrl_GeneralShape()); container.list.add(ctrl); } offset += Ctrl_ShapePic.parseElement((Ctrl_ShapePic) ctrl, size, buf, offset, version); } break; case HWPTAG_SHAPE_COMPONENT_LINE: { Ctrl_GeneralShape ctrl = null; if (container.list != null) { Optional opCtrl = container.list.stream() .filter(c -> (c instanceof Ctrl_ShapeLine)).reduce((a, b) -> b); if (opCtrl.isPresent()) { ctrl = opCtrl.get(); } } if (ctrl == null) { ctrl = new Ctrl_ShapeLine(new Ctrl_GeneralShape()); container.list.add(ctrl); } offset += Ctrl_ShapeLine.parseElement((Ctrl_ShapeLine) ctrl, size, buf, offset, version); } break; case HWPTAG_SHAPE_COMPONENT_RECTANGLE: { Ctrl_GeneralShape ctrl = null; if (container.list != null) { Optional opCtrl = container.list.stream() .filter(c -> (c instanceof Ctrl_ShapeRect)).reduce((a, b) -> b); if (opCtrl.isPresent()) { ctrl = opCtrl.get(); } } if (ctrl == null) { ctrl = new Ctrl_ShapeRect(new Ctrl_GeneralShape()); container.list.add(ctrl); } offset += Ctrl_ShapeRect.parseElement((Ctrl_ShapeRect) ctrl, size, buf, offset, version); } break; case HWPTAG_SHAPE_COMPONENT_ELLIPSE: { Ctrl_GeneralShape ctrl = null; if (container.list != null) { Optional opCtrl = container.list.stream() .filter(c -> (c instanceof Ctrl_ShapeEllipse)).reduce((a, b) -> b); if (opCtrl.isPresent()) { ctrl = opCtrl.get(); } } if (ctrl == null) { ctrl = new Ctrl_ShapeEllipse(new Ctrl_GeneralShape()); container.list.add(ctrl); } offset += Ctrl_ShapeEllipse.parseElement((Ctrl_ShapeEllipse) ctrl, size, buf, offset, version); } break; case HWPTAG_SHAPE_COMPONENT_ARC: { Ctrl_GeneralShape ctrl = null; if (container.list != null) { Optional opCtrl = container.list.stream() .filter(c -> (c instanceof Ctrl_ShapeArc)).reduce((a, b) -> b); if (opCtrl.isPresent()) { ctrl = opCtrl.get(); } } if (ctrl == null) { ctrl = new Ctrl_ShapeArc(new Ctrl_GeneralShape()); container.list.add(ctrl); } offset += Ctrl_ShapeArc.parseElement((Ctrl_ShapeArc) ctrl, size, buf, offset, version); } break; case HWPTAG_SHAPE_COMPONENT_POLYGON: { Ctrl_GeneralShape ctrl = null; if (container.list != null) { Optional opCtrl = container.list.stream() .filter(c -> (c instanceof Ctrl_ShapePolygon)).reduce((a, b) -> b); if (opCtrl.isPresent()) { ctrl = opCtrl.get(); } } if (ctrl == null) { ctrl = new Ctrl_ShapePolygon(new Ctrl_GeneralShape()); container.list.add(ctrl); } offset += Ctrl_ShapePolygon.parseElement((Ctrl_ShapePolygon) ctrl, size, buf, offset, version); } break; case HWPTAG_SHAPE_COMPONENT_CURVE: { Ctrl_GeneralShape ctrl = null; if (container.list != null) { Optional opCtrl = container.list.stream() .filter(c -> (c instanceof Ctrl_ShapeCurve)).reduce((a, b) -> b); if (opCtrl.isPresent()) { ctrl = opCtrl.get(); } } if (ctrl == null) { ctrl = new Ctrl_ShapeCurve(new Ctrl_GeneralShape()); container.list.add(ctrl); } offset += Ctrl_ShapeCurve.parseElement((Ctrl_ShapeCurve) ctrl, size, buf, offset, version); } break; case HWPTAG_SHAPE_COMPONENT_OLE: { Ctrl_GeneralShape ctrl = null; if (container.list != null) { Optional opCtrl = container.list.stream() .filter(c -> (c instanceof Ctrl_ShapeOle)).reduce((a, b) -> b); if (opCtrl.isPresent()) { ctrl = opCtrl.get(); } } if (ctrl == null) { ctrl = new Ctrl_ShapeOle(new Ctrl_GeneralShape()); container.list.add(ctrl); } offset += Ctrl_ShapeOle.parseElement((Ctrl_ShapeOle) ctrl, size, buf, offset, version); } break; case HWPTAG_EQEDIT: { Ctrl_GeneralShape ctrl = null; if (container.list != null) { Optional opCtrl = container.list.stream() .filter(c -> (c instanceof Ctrl_EqEdit)).reduce((a, b) -> b); if (opCtrl.isPresent()) { ctrl = opCtrl.get(); } } if (ctrl == null) { ctrl = new Ctrl_EqEdit(new Ctrl_GeneralShape()); container.list.add(ctrl); } offset += Ctrl_EqEdit.parseElement((Ctrl_EqEdit) ctrl, size, buf, offset, version); } break; case HWPTAG_SHAPE_COMPONENT_TEXTART: { Ctrl_GeneralShape ctrl = null; if (container.list != null) { Optional opCtrl = container.list.stream() .filter(c -> (c instanceof Ctrl_ShapeTextArt)).reduce((a, b) -> b); if (opCtrl.isPresent()) { ctrl = opCtrl.get(); } } if (ctrl == null) { ctrl = new Ctrl_ShapeTextArt(new Ctrl_GeneralShape()); container.list.add(ctrl); } offset += Ctrl_ShapeTextArt.parseElement((Ctrl_ShapeTextArt) ctrl, size, buf, offset, version); } break; case HWPTAG_LIST_HEADER: { Ctrl_Common ctrl = container.list.stream().reduce((a, b) -> b).get(); int subParaCount = HwpRecord_ListHeader.getCount(tagNum, level, size, buf, offset, version); offset += 6; // 문단수 2byte, 속성 4byte if (ctrl instanceof Ctrl_ShapeRect) { Ctrl_Common ctrlCmn = (Ctrl_Common) ctrl; ctrlCmn.ctrlId = "cer$"; offset -= 6; ctrlCmn.textVerAlign = HwpRecord_ListHeader.getVertAlign(6, buf, offset, version); offset += 6; offset += parseListAppend(ctrlCmn, size - 6, buf, offset, version); offset += parseCtrlRecurse(ctrl, level, buf, offset, version); } else if (ctrl instanceof Ctrl_ShapePolygon) { Ctrl_Common ctrlCmn = (Ctrl_Common) ctrl; ctrlCmn.ctrlId = "lop$"; offset -= 6; ctrlCmn.textVerAlign = HwpRecord_ListHeader.getVertAlign(6, buf, offset, version); offset += 6; offset += parseListAppend(ctrlCmn, size - 6, buf, offset, version); offset += parseCtrlRecurse(ctrl, level, buf, offset, version); } else { // container내 도형의 caption은 무시하자. offset += (size - 6); } } break; case HWPTAG_SHAPE_COMPONENT: Ctrl_GeneralShape baseCtrl = new Ctrl_GeneralShape(); Ctrl_GeneralShape newCtrl = Ctrl_GeneralShape.parse(baseCtrl, size, buf, offset, version); offset += size; if (newCtrl instanceof Ctrl_GeneralShape) { container.list.add((Ctrl_GeneralShape) newCtrl); if (newCtrl instanceof Ctrl_Container) { if (((Ctrl_Container) newCtrl).list == null) ((Ctrl_Container) newCtrl).list = new ArrayList(); offset += parseContainerRecurse((Ctrl_Container) newCtrl, level, buf, offset, version); } } break; case HWPTAG_SHAPE_COMPONENT_UNKNOWN: case HWPTAG_TABLE: case HWPTAG_PARA_HEADER: case HWPTAG_PARA_TEXT: case HWPTAG_PAGE_DEF: case HWPTAG_FOOTNOTE_SHAPE: case HWPTAG_PAGE_BORDER_FILL: case HWPTAG_PARA_RANGE_TAG: case HWPTAG_CTRL_DATA: case HWPTAG_FORM_OBJECT: case HWPTAG_MEMO_SHAPE: case HWPTAG_MEMO_LIST: case HWPTAG_CHART_DATA: case HWPTAG_VIDEO_DATA: default: offset += size; break; } } else if (level == runLevel) { offset += headerOffset; log.fine(IntStream.rangeClosed(0, level).mapToObj(i -> String.valueOf(i)).collect(Collectors.joining()) + "[TAG]=" + tag.toString() + " (" + size + ") = runLevel"); switch (tag) { case HWPTAG_PARA_HEADER: case HWPTAG_CTRL_HEADER: case HWPTAG_PAGE_DEF: case HWPTAG_FOOTNOTE_SHAPE: case HWPTAG_PAGE_BORDER_FILL: case HWPTAG_TABLE: case HWPTAG_LIST_HEADER: case HWPTAG_VIDEO_DATA: case HWPTAG_FORM_OBJECT: case HWPTAG_CTRL_DATA: offset += size; break; case HWPTAG_SHAPE_COMPONENT: Ctrl_GeneralShape baseCtrl = new Ctrl_GeneralShape(); Ctrl_GeneralShape newCtrl = Ctrl_GeneralShape.parse(baseCtrl, size, buf, offset, version); offset += size; if (newCtrl instanceof Ctrl_GeneralShape) { container.list.add((Ctrl_GeneralShape) newCtrl); if (newCtrl instanceof Ctrl_Container) { if (((Ctrl_Container) newCtrl).list == null) ((Ctrl_Container) newCtrl).list = new ArrayList(); offset += parseContainerRecurse((Ctrl_Container) newCtrl, level, buf, offset, version); } } break; case HWPTAG_SHAPE_COMPONENT_PICTURE: case HWPTAG_SHAPE_COMPONENT_LINE: case HWPTAG_SHAPE_COMPONENT_RECTANGLE: case HWPTAG_SHAPE_COMPONENT_ELLIPSE: case HWPTAG_SHAPE_COMPONENT_ARC: case HWPTAG_SHAPE_COMPONENT_POLYGON: case HWPTAG_SHAPE_COMPONENT_CURVE: case HWPTAG_SHAPE_COMPONENT_OLE: case HWPTAG_EQEDIT: case HWPTAG_SHAPE_COMPONENT_TEXTART: case HWPTAG_SHAPE_COMPONENT_UNKNOWN: default: offset += size; } } } return offset - off; } private int parseListAppend(Ctrl_Common obj, int size, byte[] buf, int off, int version) throws HwpParseException { int len = 0; switch (obj.ctrlId) { case "cer$": len = Ctrl_ShapeRect.parseListHeaderAppend((Ctrl_ShapeRect) obj, size, buf, off, version); break; case " osg": len = Ctrl_GeneralShape.parseListHeaderAppend((Ctrl_GeneralShape) obj, size, buf, off, version); break; case " lbt": len = Ctrl_Table.parseListHeaderAppend((Ctrl_Table) obj, size, buf, off, version); break; case "deqe": len = Ctrl_EqEdit.parseListHeaderAppend((Ctrl_EqEdit) obj, size, buf, off, version); break; case "lop$": len = Ctrl_ShapePolygon.parseListHeaderAppend((Ctrl_ShapePolygon) obj, size, buf, off, version); break; case "lle$": len = Ctrl_ShapeEllipse.parseListHeaderAppend((Ctrl_ShapeEllipse) obj, size, buf, off, version); break; } return len; } private int parseListAppend(Ctrl obj, int size, byte[] buf, int off, int version) throws HwpParseException { int len = 0; switch (obj.ctrlId) { case "dces": off += (size - 6); len = size; break; case "daeh": case "toof": len = Ctrl_HeadFoot.parseListHeaderAppend((Ctrl_HeadFoot) obj, size, buf, off, version); // 문서내 14byte 내용은 있으나 28byte는 정의가 되지 않았다. 이중 10byte는 해석이 가능. offset값은 임의로 만든다. off += (size - 6); len = size; break; case " nf": len = size; break; } return len; } } H2Orestart-0.7.2/source/HwpDoc/HwpxFile.java000066400000000000000000000155671476273367000206270ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; import java.util.zip.DataFormatException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Document; import org.xml.sax.SAXException; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; import HwpDoc.Exception.OwpmlParseException; import HwpDoc.OCFdoc.OwpmlFile; import HwpDoc.OLEdoc.DirectoryEntry; import HwpDoc.paragraph.HwpParagraph; public class HwpxFile { private static final Logger log = Logger.getLogger(HwpxFile.class.getName()); public String filename; public OwpmlFile owplmFile; public HwpFileHeader fileHeader; public int version; public HwpDocInfo docInfo; public List sections; // Let's have member that are needed for showing in LibreOffice public List directoryBinData; public List paraList; public HwpxFile(String filename) throws FileNotFoundException { this.filename = filename; owplmFile = new OwpmlFile(this.filename); fileHeader = new HwpFileHeader(); docInfo = new HwpDocInfo(this); sections = new ArrayList(); } public HwpxFile(File file) throws FileNotFoundException { owplmFile = new OwpmlFile(file); this.filename = file.toString(); fileHeader = new HwpFileHeader(); docInfo = new HwpDocInfo(this); sections = new ArrayList(); } public OwpmlFile getOwpmlFile() { return owplmFile; } public List getSections() { return sections; } public boolean detect() throws HwpDetectException, IOException { // read CompoundFile structure try { owplmFile.open(); if (getFileHeader() == false) { owplmFile.close(); // throw new CompoundParseException(); } } catch (ParserConfigurationException | SAXException | DataFormatException e) { owplmFile.close(); throw new HwpDetectException(ErrCode.INVALID_ZIP_DATA_FORMAT); } catch (HwpDetectException e) { owplmFile.close(); throw new HwpDetectException(e.getReason()); } log.fine("Header parsed"); return true; } public void open() throws HwpDetectException, IOException, DataFormatException, ParserConfigurationException, SAXException, OwpmlParseException, HwpParseException, NotImplementedException { if (fileHeader.version==null) { detect(); } version = Integer.parseInt(fileHeader.version); if (getDocInfo(version)==false) throw new OwpmlParseException(); log.fine("DocInfo parsed"); // Contents/SectionX.xml 을 읽는다. for (String section: owplmFile.getSections()) { readSection(section, version); } } public boolean getFileHeader() throws HwpDetectException, IOException, ParserConfigurationException, SAXException, DataFormatException { return fileHeader.parse(getDocument("version.xml")); } public HwpDocInfo getDocInfo() { return docInfo; } public boolean getDocInfo(int version) throws IOException, DataFormatException, ParserConfigurationException, SAXException, HwpParseException, NotImplementedException { if (docInfo.readContentHpf(getDocument("Contents/content.hpf"), version)) { return docInfo.read(getDocument("Contents/header.xml"), version); } else { return false; } } public boolean readSection(String name, int version) throws IOException, DataFormatException, ParserConfigurationException, SAXException, NotImplementedException { Document document = getDocument(name); HwpSection hwpSection = new HwpSection(this); hwpSection.read(document, version); sections.add(hwpSection); return true; } public Document getDocument(String entryName) throws IOException, ParserConfigurationException, SAXException, DataFormatException { InputStream is = owplmFile.getInputStream(entryName); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); return builder.parse(is); } public void close() throws IOException { owplmFile.close(); } public String findBinData(String shortName) { return owplmFile.getBinData(shortName); } public byte[] getBinDataByIDRef(String shortName) throws IOException, DataFormatException { String entry = owplmFile.getBinData(shortName); return owplmFile.getBytes(entry); } public byte[] getBinDataByEntry(String entry) throws IOException, DataFormatException { return owplmFile.getBytes(entry); } public List getParaList() { return paraList; } public void addParaList(HwpParagraph para) { if (this.paraList == null) this.paraList = new ArrayList(); this.paraList.add(para); } public static class Rand { static int random_seed; public static void srand(int seed) { random_seed = seed; } public static int rand() { random_seed = (random_seed * 214013 + 2531011) & 0xFFFFFFFF; return ((random_seed >> 16) & 0x7FFF); } } } H2Orestart-0.7.2/source/HwpDoc/OCFdoc/000077500000000000000000000000001476273367000173155ustar00rootroot00000000000000H2Orestart-0.7.2/source/HwpDoc/OCFdoc/OwpmlFile.java000066400000000000000000000131271476273367000220620ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.OCFdoc; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.util.HashMap; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import java.util.zip.DataFormatException; import java.util.zip.Inflater; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; public class OwpmlFile { private HashMap offsetMap = new HashMap<>(); private File file; public OwpmlFile(String filename) throws FileNotFoundException { this(new File(filename)); } public OwpmlFile(File file) throws FileNotFoundException { this.file = file; } public void open() { try (FileInputStream fileInputStream = new FileInputStream(this.file); ZipInputStream zipInputStream = new ZipInputStream(fileInputStream)) { ZipEntry zipEntry = null; long entryOffset = 0; while ((zipEntry = zipInputStream.getNextEntry()) != null) { entryOffset += 30 + zipEntry.getName().length() + (zipEntry.getExtra()==null ? 0 : zipEntry.getExtra().length); long offsetStart = entryOffset; entryOffset += zipEntry.getCompressedSize(); long offsetEnd = entryOffset; int zipMethod = zipEntry.getMethod(); offsetMap.put(zipEntry.getName(), new Offset(offsetStart, offsetEnd, zipMethod)); zipInputStream.closeEntry(); } } catch (IOException e) { e.printStackTrace(); } } public InputStream getInputStream(String entryName) throws IOException, DataFormatException { Offset offset = offsetMap.get(entryName); if (offset == null) { throw new DataFormatException(); } long entrySize = (int)(offset.end - offset.start); byte[] buf = new byte[(int)entrySize]; try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { raf.seek(offset.start); int readLen = raf.read(buf, 0, (int)entrySize); if (offset.zipMethod == ZipEntry.DEFLATED) { buf = unzip(buf, readLen); } } return new ByteArrayInputStream(buf); } public String findBinData(String shortName) { return null; } public byte[] getBytes(String entryName) throws IOException, DataFormatException { Offset offset = offsetMap.get(entryName); long entrySize = (int)(offset.end - offset.start); byte[] buf = new byte[(int)entrySize]; try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { raf.seek(offset.start); int readLen = raf.read(buf, 0, (int)entrySize); if (offset.zipMethod == ZipEntry.DEFLATED) { buf = unzip(buf, readLen); } } return buf; } public List getSections() { List sections = offsetMap.keySet().stream().filter(s -> s.contains("section")).sorted().collect(Collectors.toList()); return sections; } public String getBinData(String shortName) { Optional binData = offsetMap.keySet().stream().filter(s -> s.startsWith("BinData")) .filter(s -> s.contains(shortName + ".")).findAny(); return binData.orElse(""); } private byte[] unzip(byte[] input, int inLen) throws IOException, DataFormatException { Inflater decompressor = new Inflater(true); decompressor.setInput(input, 0, input.length); ByteArrayOutputStream bos = new ByteArrayOutputStream(inLen); // Decompress the data byte[] buf = new byte[8096]; while (!decompressor.finished()) { int count = decompressor.inflate(buf); if (count > 0) { bos.write(buf, 0, count); } else { throw new IOException("can't decompress data"); } } bos.close(); return bos.toByteArray(); } public void close() throws IOException { } public static class Offset { long start; long end; int zipMethod; public Offset(long start, long end, int zipMethod) { this.start = start; this.end = end; this.zipMethod = zipMethod; } } } H2Orestart-0.7.2/source/HwpDoc/OLEdoc/000077500000000000000000000000001476273367000173255ustar00rootroot00000000000000H2Orestart-0.7.2/source/HwpDoc/OLEdoc/CompoundEntry.java000066400000000000000000000022371476273367000230020ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.OLEdoc; import java.util.List; public interface CompoundEntry { String getEntryName(); String getEntryType(); List getChildEntries(); } H2Orestart-0.7.2/source/HwpDoc/OLEdoc/CompoundFile.java000066400000000000000000000544501476273367000225640ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.OLEdoc; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import HwpDoc.ErrCode; import HwpDoc.Exception.CompoundDetectException; public class CompoundFile { private static final Logger log = Logger.getLogger(CompoundFile.class.getName()); private RandomAccessFile raf; private int minorVersion; private int majorVersion; private int sectorSize = 512; private int shortSectorSize; private int num_Directory; // Support only in version 4 private int num_SAT; private int first_SecID_Directory; private int miniStreamCutoffSize; private int first_SecID_SSAT; private int num_SSAT; private int first_SecID_MSAT; private int num_MSAT; private ArrayList sectorList; private ArrayList SAT_list; // Master SAT private ArrayList SSAT_SecID_list; private ArrayList Directory_SecID_list; private ArrayList SStream_SecID_list; private ArrayList SStream_list; private ArrayList DirectoryEntry_list; final static byte[] COMPOUND_SIGANTURE = { (byte)0xD0, (byte)0xCF, (byte)0x11, (byte)0xE0, (byte)0xA1, (byte)0xB1, (byte)0x1A, (byte)0xE1 }; public CompoundFile(String filename) throws FileNotFoundException { this(new File(filename)); } public CompoundFile(File file) throws FileNotFoundException { raf = new RandomAccessFile(file, "r"); sectorList = new ArrayList(); SAT_list = new ArrayList(); SSAT_SecID_list = new ArrayList(); Directory_SecID_list = new ArrayList(); SStream_SecID_list = new ArrayList(); SStream_list = new ArrayList(); DirectoryEntry_list = new ArrayList(); } private void addSiblings(List indexList, int currentIndex) { if (currentIndex==-1) return; if (!indexList.contains(currentIndex)) { indexList.add(currentIndex); } int leftSibling = DirectoryEntry_list.size()>currentIndex?DirectoryEntry_list.get(currentIndex).leftSiblingID:-1; int rightSibling = DirectoryEntry_list.size()>currentIndex?DirectoryEntry_list.get(currentIndex).rightSiblingID:-1; if (rightSibling!=-1) { int elderIndex = indexList.indexOf(currentIndex); indexList.add(elderIndex+1, rightSibling); if (rightSibling < DirectoryEntry_list.size()) { addSiblings(indexList, rightSibling); } } if (leftSibling!=-1) { int elderIndex = indexList.indexOf(currentIndex); indexList.add(elderIndex, leftSibling); if (leftSibling < DirectoryEntry_list.size()) { addSiblings(indexList, leftSibling); } } } public DirectoryEntry getEntry(String entryName) { Optional op = DirectoryEntry_list.stream() .filter(e -> e.directoryEntryName.trim().equals(entryName)) .findFirst(); return op.isPresent()?op.get():null; } public List getChildEntries(DirectoryEntry baseEntry) { List entryIdx = new LinkedList(); int index = 0; if (baseEntry == null) { index = DirectoryEntry_list.get(0).childID; } else { index = baseEntry.childID; } addSiblings(entryIdx, index); List entries = entryIdx.stream() .filter(i -> (i DirectoryEntry_list.get(i)) .collect(Collectors.toList()); return entries; } public List getChildEntries(String baseEntryName) { Optional op = DirectoryEntry_list.stream() .filter(e -> e.directoryEntryName.trim().equals(baseEntryName)) .findFirst(); if (op.isPresent()) { return getChildEntries(op.get()); } else { return new ArrayList(); } } public byte[] getComponent(String entryName) throws CompoundDetectException { DirectoryEntry entry = getEntry(entryName); if (entry!=null) { return read(entry); } else { throw new CompoundDetectException(); } } public byte[] read(DirectoryEntry entry) { byte[] buf = new byte[(int)entry.streamSize]; int buff_offset = 0; int len = 0; List streamContainerSectors = null; if (entry.streamSize=64?64:remainSize); if (readLen<0) continue; remainSize -= readLen; // writeToBuffer System.arraycopy(b, 0, buf, buff_offset, readLen); buff_offset += readLen; } catch (IOException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } } } else { // normal Stream byte[] b = new byte[sectorSize]; int remainSize = (int)entry.streamSize; streamContainerSectors = entry.secNums; for (int secNum: entry.secNums) { if (secNum==0xFFFFFFFE) continue; // readStream try { raf.seek((secNum+1)*sectorSize); int readLen = raf.read(b, 0, remainSize>=sectorSize?sectorSize:remainSize); remainSize -= readLen; // writeToBuf System.arraycopy(b, 0, buf, buff_offset, readLen); buff_offset += readLen; } catch (IOException e1) { e1.printStackTrace(); } } } return buf; } public void open() throws CompoundDetectException, IOException { byte[] buf = new byte[sectorSize]; // from Signature to Number of DIFAT sectors if (raf.read(buf, 0, sectorSize) != sectorSize) { throw new CompoundDetectException(ErrCode.FILE_READ_ERROR); } parse_Header(buf); if (majorVersion == 0x0004) { raf.seek(4096); sectorSize = 4096; buf = new byte[sectorSize]; } // collect MSAT SecID int secID = first_SecID_MSAT; if (secID != 0xFFFFFFFE && secID != 0xFFFFFFFF) { // [20211103] 0xFFFFFFFF 조건 추가. (국방CBD방법론v1(1권) 읽지 못하는 이슈 수정) read_MSAT_sector(secID); // MSAT sector에서 SSAT SecID들을 구한다. } if (log.isLoggable(Level.FINEST)) { log.finest("[______SAT Sector]={"+ SAT_list.stream().map(i -> i.toString()).collect(Collectors.joining(",")) + "}"); } // Directory secID = first_SecID_Directory; Directory_SecID_list.add(secID); while(secID != 0xFFFFFFFE) { int satIndex = secID/(sectorSize/4); // collect Directory SecID int satID = SAT_list.get(secID/(sectorSize/4)); List secID_list = get_SecIDs_from_SAT(satID, satIndex, secID); secID_list.stream().filter(id -> !Directory_SecID_list.contains(id)).forEach(id -> Directory_SecID_list.add(id)); secID = secID_list.get(secID_list.size()-1); } // collect Directory Entries for (int secID_Directory: Directory_SecID_list) { if (secID_Directory != 0xFFFFFFFE) { read_Directory_sector(secID_Directory); } } if (log.isLoggable(Level.FINEST)) { log.finest("[Directory Sector]={" + Directory_SecID_list.stream().map(i -> i.toString()).collect(Collectors.joining(",")) + "}"); } // collect SSAT SecID int lastSecID = first_SecID_SSAT; SSAT_SecID_list.add(lastSecID); while(lastSecID != 0xFFFFFFFE) { int satIndex = lastSecID/(sectorSize/4); int satID = SAT_list.get(satIndex); List secID_list = get_SecIDs_from_SAT(satID, satIndex, lastSecID); // SAT를 읽어서 SSAT SecID 들을 구한다. secID_list.stream().filter(id -> !SSAT_SecID_list.contains(id)).forEach(id -> SSAT_SecID_list.add(id)); lastSecID = secID_list.get(secID_list.size()-1); } if (log.isLoggable(Level.FINEST)) { log.finest("[Short SAT Sector]={" +SSAT_SecID_list.stream().map(i->i.toString()).collect(Collectors.joining(",")) + "}"); } int SecID_SStream = 0; // collect SecID of Stream for each Directory entries for (DirectoryEntry entry: DirectoryEntry_list) { if (entry.objectType == 0x05) { // Root Storage // Short Stream container Stream SecID lastSecID = entry.startingSectorID; entry.secNums = new ArrayList(); entry.secNums.add(lastSecID); while(lastSecID != 0xFFFFFFFE) { int containerIndex = lastSecID/(sectorSize/4); int satID = SAT_list.get(containerIndex); List secID_list = get_SecIDs_from_SAT(satID, containerIndex, lastSecID); // SAT를 읽어서 stream container SecID 들을 구한다. secID_list.stream().filter(id -> !entry.secNums.contains(id)).forEach(id -> entry.secNums.add(id)); lastSecID = secID_list.get(secID_list.size()-1); } } else if (entry.objectType == 0x02) { // Stream entry.secNums = new ArrayList(); entry.secNums.add(entry.startingSectorID); if (entry.streamSize secID_list = get_SecIDs_from_SAT(ssatID, ssatIndex, lastSSecID); secID_list.stream().filter(id -> !entry.secNums.contains(id)).forEach(id -> entry.secNums.add(id)); lastSSecID = entry.secNums.get(entry.secNums.size()-1); } } else { // Stream int last_SecID = entry.startingSectorID; while(last_SecID != 0xFFFFFFFE) { int satIndex = last_SecID/(sectorSize/4); int satID = SAT_list.get(satIndex); List secID_list = get_SecIDs_from_SAT(satID, satIndex, last_SecID); secID_list.stream().filter(id -> !entry.secNums.contains(id)).forEach(id -> entry.secNums.add(id)); last_SecID = entry.secNums.get(entry.secNums.size()-1); } } } else { continue; } } if (log.isLoggable(Level.FINEST)) { log.finest("_I __________Name_______ ___Type LS RS Chd Sec Size__ Chain__________"); for (int idx=0; idx i.toString()).collect(Collectors.joining(","))) ); } } log.fine("open() closing"); } private List get_SecIDs_from_SAT(int secID, int satIndex, int secID_SSAT) throws IOException { byte[] buf = new byte[sectorSize]; raf.seek((secID+1) * sectorSize); raf.read(buf, 0, sectorSize); int currSecID = secID_SSAT; int iBuf = currSecID%(sectorSize/4) * 4; List secIdList = new ArrayList(); while((sectorSize/4)*(satIndex) <= currSecID && currSecID < (sectorSize/4)*(satIndex+1)) { currSecID = buf[iBuf+3]<<24&0xFF000000 | buf[iBuf+2]<<16&0xFF0000 | buf[iBuf+1]<<8&0xFF00 | buf[iBuf]&0xFF; if (currSecID == 0xFFFFFFFE) { secIdList.add(currSecID); break; } secIdList.add(currSecID); iBuf = currSecID%(sectorSize/4) * 4; } return secIdList; } public void parseSectors(int secID, byte[] buf) throws CompoundDetectException, IOException { int sectorType = buf[3]<<24&0xFF000000 | buf[2]<<16&0xFF0000 | buf[1]<<8&0xFF00 | buf[0]&0xFF; // for DIFAT, FAT, MiniFAT, if (SAT_list.contains(secID)) { parse_SAT_sector(secID, buf); } if (secID < first_SecID_Directory) { if (secID == first_SecID_SSAT) { parse_SSAT_sector(buf); } else { } } } private void parse_SAT_sector(int secID, byte[] buf) { Map> multiMap = new HashMap>(); for(int i=secID*(sectorSize/4), iBuf=0; iBuf < sectorSize-4; i++,iBuf+=4) { int nextSecID = buf[iBuf+3]<<24&0xFF000000 | buf[iBuf+2]<<16&0xFF0000 | buf[iBuf+1]<<8&0xFF00 | buf[iBuf]&0xFF; Sector sectorInChain = new Sector(); sectorInChain.sectorNum = i; switch(nextSecID) { case 0xFFFFFFFC: // DIFAT sectorInChain.type = SectorType.MSAT; break; case 0xFFFFFFFD: // FAT sectorInChain.type = SectorType.SAT; break; case 0xFFFFFFFA: // maximum regular sector number case 0xFFFFFFFE: // ENDOFCHAIN sectorInChain.type = SectorType.ENDOFCHAIN; // Directory // FAT // MiniFAT // DIFAT // Stream (User-Defined Data) // Range Lock break; case 0xFFFFFFFF: // unallocated sectorInChain.type = SectorType.FREE; break; case 0xFFFFFFFB: // Reserved for future use. sectorInChain.type = SectorType.CONTINUE; break; default: sectorInChain.type = SectorType.CONTINUE; sectorInChain.nextNum = nextSecID; } if (nextSecID < 0xFFFFFFFF) sectorList.add(sectorInChain); } } private SectorType lookupSectorType(byte[] buf) { for(int i=0, index=0; index < sectorSize-4; i++,index+=4) { int sector = buf[index+3]<<24&0xFF000000 | buf[index+2]<<16&0xFF0000 | buf[index+1]<<8&0xFF00 | buf[index]&0xFF; if (sector == 0xFFFFFFFC) return SectorType.MSAT; else if (sector == 0xFFFFFFFD) return SectorType.SAT; } return SectorType.CONTINUE; } private void read_Directory_sector(int secID) throws IOException { byte[] buf = new byte[sectorSize]; raf.seek((secID+1) * sectorSize); if (raf.read(buf, 0, sectorSize) == sectorSize) { parse_Directory_sector(buf); } } private void parse_Directory_sector(byte[] buf) throws UnsupportedEncodingException { for(int i=0, index=0; index <= sectorSize-128; i++,index+=128) { int entryNameLen = buf[index+65]<<8&0xFF00 | buf[index+64]&0xFF; String directoryEntryName = new String(buf, index, entryNameLen, StandardCharsets.UTF_16LE); int objectType = buf[index+66]&0xFF; int colorFlag = buf[index+67]&0xFF; int leftSiblingID = buf[index+71]<<24&0xFF000000 | buf[index+70]<<16&0xFF0000 | buf[index+69]<<8&0xFF00 | buf[index+68]&0xFF; int rightSiblingID = buf[index+75]<<24&0xFF000000 | buf[index+74]<<16&0xFF0000 | buf[index+73]<<8&0xFF00 | buf[index+72]&0xFF; int childID = buf[index+79]<<24&0xFF000000 | buf[index+78]<<16&0xFF0000 | buf[index+77]<<8&0xFF00 | buf[index+76]&0xFF; long clsID1 = buf[index+87]<<24&0xFF000000 | buf[index+86]<<16&0xFF0000 | buf[index+85]<<8&0xFF00 | buf[index+84]&0xFF; clsID1 = clsID1<<32 | buf[index+83]<<24&0xFF000000 | buf[index+82]<<16&0xFF0000 | buf[index+81]<<8&0xFF00 | buf[index+80]&0xFF; long clsID2 = buf[index+95]<<24&0xFF000000 | buf[index+94]<<16&0xFF0000 | buf[index+93]<<8&0xFF00 | buf[index+92]&0xFF; clsID2 = clsID2<<32 | buf[index+91]<<24&0xFF000000 | buf[index+90]<<16&0xFF0000 | buf[index+89]<<8&0xFF00 | buf[index+88]&0xFF; int stateBit = buf[index+99]<<24&0xFF000000 | buf[index+98]<<16&0xFF0000 | buf[index+97]<<8&0xFF00 | buf[index+96]&0xFF; long creationTime = buf[index+107]<<24&0xFF000000 | buf[index+106]<<16&0xFF0000 | buf[index+105]<<8&0xFF00 | buf[index+104]&0xFF; creationTime = creationTime<<32 | buf[index+103]<<24&0xFF000000 | buf[index+102]<<16&0xFF0000 | buf[index+101]<<8&0xFF00 | buf[index+100]&0xFF; long modifiedTime = buf[index+115]<<24&0xFF000000 | buf[index+114]<<16&0xFF0000 | buf[index+113]<<8&0xFF00 | buf[index+112]&0xFF; modifiedTime = modifiedTime<<32 | buf[index+111]<<24&0xFF000000 | buf[index+110]<<16&0xFF0000 | buf[index+109]<<8&0xFF00 | buf[index+108]&0xFF; int startingSectorID = buf[index+119]<<24&0xFF000000 | buf[index+118]<<16&0xFF0000 | buf[index+117]<<8&0xFF00 | buf[index+116]&0xFF; long streamSize = buf[index+127]<<24&0xFF000000 | buf[index+126]<<16&0xFF0000 | buf[index+125]<<8&0xFF00 | buf[index+124]&0xFF; streamSize = streamSize<<32 | buf[index+123]<<24&0xFF000000 | buf[index+122]<<16&0xFF0000 | buf[index+121]<<8&0xFF00 | buf[index+120]&0xFF; DirectoryEntry de = new DirectoryEntry(directoryEntryName, objectType, colorFlag, leftSiblingID, rightSiblingID, childID, clsID1, clsID2, stateBit, creationTime, modifiedTime, startingSectorID, streamSize); DirectoryEntry_list.add(de); } } private void read_SSAT_sector(int secID) throws IOException { byte[] buf = new byte[sectorSize]; raf.seek((secID+1) * sectorSize); if (raf.read(buf, 0, sectorSize) == sectorSize) { parse_SSAT_sector(buf); } } private void parse_SSAT_sector(byte[] buf) { int offset = 0; while(offset */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.OLEdoc; import java.util.List; public class DirectoryEntry { String directoryEntryName; int objectType; int colorFlag; int leftSiblingID; int rightSiblingID; int childID; long clsID1; long clsID2; int stateBit; long creationTime; long modifiedTime; int startingSectorID; long streamSize; List secNums; public DirectoryEntry(String directoryEntryName, int objectType, int colorFlag, int leftSiblingID, int rightSiblingID, int childID, long clsID1, long clsID2, int stateBit, long creationTime, long modifiedTime, int startingSectorID, long streamSize) { this.directoryEntryName = directoryEntryName; this.objectType = objectType; this.colorFlag = colorFlag; this.leftSiblingID = leftSiblingID; this.rightSiblingID = rightSiblingID; this.childID = childID; this.clsID1 = clsID1; this.clsID2 = clsID2; this.stateBit = stateBit; this.creationTime = creationTime; this.modifiedTime = modifiedTime; this.startingSectorID = startingSectorID; this.streamSize = streamSize; } public int getObjectType() { return objectType; } public String getDirectoryEntryName() { return directoryEntryName; } } H2Orestart-0.7.2/source/HwpDoc/OLEdoc/Sector.java000066400000000000000000000021061476273367000214260ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.OLEdoc; public class Sector { SectorType type; int sectorNum; int nextNum; } H2Orestart-0.7.2/source/HwpDoc/OLEdoc/SectorType.java000066400000000000000000000021121476273367000222650ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.OLEdoc; public enum SectorType { SAT, MSAT, ENDOFCHAIN, CONTINUE, FREE, MAX } H2Orestart-0.7.2/source/HwpDoc/paragraph/000077500000000000000000000000001476273367000201655ustar00rootroot00000000000000H2Orestart-0.7.2/source/HwpDoc/paragraph/CapParagraph.java000066400000000000000000000024331476273367000233630ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import org.w3c.dom.Node; import HwpDoc.Exception.NotImplementedException; public class CapParagraph extends HwpParagraph { public CapParagraph() { super(); } public CapParagraph(Node node, int version) throws NotImplementedException { super(node, version); } } H2Orestart-0.7.2/source/HwpDoc/paragraph/CellParagraph.java000066400000000000000000000024421476273367000235370ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import org.w3c.dom.Node; import HwpDoc.Exception.NotImplementedException; public class CellParagraph extends HwpParagraph { public CellParagraph() { super(); } public CellParagraph(Node node, int version) throws NotImplementedException { super(node, version); } } H2Orestart-0.7.2/source/HwpDoc/paragraph/CharShape.java000066400000000000000000000127571476273367000227020ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Optional; import java.util.logging.Logger; import HwpDoc.Exception.HwpParseException; public class CharShape { private static final Logger log = Logger.getLogger(CharShape.class.getName()); public int start; public int charShapeID; public static List parse(int tagNum, int level, int size, byte[] buf, int off, int version) throws HwpParseException { int offset = off; List charShapeList = new ArrayList(); while (size-(offset-off) >= 8) { CharShape shape = new CharShape(); shape.start = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; shape.charShapeID = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; log.fine(" " +"String시작위치="+shape.start +",문자모양ID="+shape.charShapeID ); charShapeList.add(shape); } if (offset-off!=size) { log.fine("[TAG]=" + tagNum + ", size=" + size + ", but currentSize=" + (offset-off)); throw new HwpParseException(); } return charShapeList; } public static int fillCharShape(int tagNum, int level, int size, byte[] buf, int off, int version, LinkedList paras) throws HwpParseException { List charShapeList = parse(tagNum, level, size, buf, off, version); if (paras != null) { Iterator iter = charShapeList.iterator(); while(iter.hasNext()) { CharShape shape = iter.next(); if (shape.start==0) { paras.stream().forEach(p -> { if (p instanceof ParaText) { ((ParaText)p).charShapeId = shape.charShapeID; } if (p instanceof Ctrl_Character) { ((Ctrl_Character)p).charShapeId = shape.charShapeID; } }); } else if (shape.start > 0) { Optional paraTextOp = paras.stream().filter(p -> (p instanceof ParaText)) .map(p -> (ParaText)p) .filter(t -> t.startIdx <= shape.start && shape.start < t.startIdx+t.text.length()) .reduce((a, b) -> b); if (paraTextOp.isPresent()) { ParaText t = paraTextOp.get(); if (t.startIdx == shape.start) { t.charShapeId = shape.charShapeID; } else { // paraText.startIdx < shape.start // split int lenToSplit = shape.start - t.startIdx; String splitLeftText = t.text.substring(0, lenToSplit); String splitRightText = t.text.substring(lenToSplit); t.text = splitLeftText; ParaText newParaText = new ParaText("____", splitRightText, shape.start, shape.charShapeID); int index = paras.indexOf(t); paras.add(index+1, newParaText); } } paras.stream().forEach(p -> { if (p instanceof ParaText) { if (((ParaText)p).startIdx > shape.start) { ((ParaText)p).charShapeId = shape.charShapeID; } } if (p instanceof Ctrl_Character) { ((Ctrl_Character)p).charShapeId = shape.charShapeID; } }); } } return paras.size(); } else { return 0; } } } H2Orestart-0.7.2/source/HwpDoc/paragraph/CommonObj.java000066400000000000000000000036211476273367000227150ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.util.List; public class CommonObj { public String ctrlId; public int objAttr; // 개체 공통 속성 (표 70참조) public int yOffset; // 세로 오프셋 값 public int xOffset; // 가로 오프셋 값 public int objWidth; // width 오브젝트의 폭 public int objHeight; // height 오브젝트의 높이 public int zOrder; public short[] objSpaces; public int objInstanceID; // 문서 내 각 개체에 대한 고유 아이디(instance ID) public int blockPageBreak; // 쪽나눔 방지 on(1)/off(0) public String objDesc; // 개체 설명문 public List paras; // LIST_HEADER 뒤에 따라오는 PARA_HEADER (복수개) public int captionAttr; // 캡션 속성 public int captionWidth; // 캡션 폭 public int captionSpacing; // 캡션과 틀 사이 간격 public int captionMaxW; // 텍스트의 최대 길이 (=개체의 폭) } H2Orestart-0.7.2/source/HwpDoc/paragraph/Ctrl.java000066400000000000000000000056171476273367000217450ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.Node; import HwpDoc.Exception.NotImplementedException; public abstract class Ctrl { private static final Logger log = Logger.getLogger(Ctrl.class.getName()); public String ctrlId; public boolean fullfilled; // 파싱이 완료되었는지를 나타냄 public Ctrl() { } public Ctrl(String ctrlId) { this.ctrlId = ctrlId; } public abstract int getSize(); public static Ctrl getCtrl(Node node, int version) throws NotImplementedException { Ctrl ctrl = null; switch(node.getNodeName()) { case "hp:colPr": ctrl = new Ctrl_ColumnDef("dloc", node, version); break; case "hp:header": ctrl = new Ctrl_HeadFoot("daeh", node, version); break; case "hp:footer": ctrl = new Ctrl_HeadFoot("toof", node, version); break; case "hp:footNote": ctrl = new Ctrl_Note(" nf", node, version); break; case "hp:endNote": ctrl = new Ctrl_Note(" ne", node, version); break; case "hp:autoNum": ctrl = new Ctrl_AutoNumber("onta", node, version); break; case "hp:newNum": ctrl = new Ctrl_NewNumber("onwn", node, version); break; case "hp:pageNum": ctrl = new Ctrl_PageNumPos("pngp", node, version); case "hp:fieldBegin": case "hp:fieldEnd": case "hp:bookmark": case "hp:pageHiding": case "hp:pageNumCtrl": case "hp:indexmark": case "hp:hiddenComment": break; case "#text": break; default: if (log.isLoggable(Level.FINE)) { throw new NotImplementedException("Ctrl"); } } return ctrl; } } H2Orestart-0.7.2/source/HwpDoc/paragraph/Ctrl_AutoNumber.java000066400000000000000000000152041476273367000240770ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.NotImplementedException; import HwpDoc.HwpElement.HwpRecordTypes.NumberShape2; public class Ctrl_AutoNumber extends Ctrl { private static final Logger log = Logger.getLogger(Ctrl_AutoNumber.class.getName()); private int size; public NumType numType; public NumberShape2 numShape; public boolean superscript; public Ctrl_AutoNumber(String ctrlId) { super(ctrlId); } public Ctrl_AutoNumber(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId); int offset = off; int attr = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; numType = NumType.from(attr&0xF); numShape = NumberShape2.from(attr>>4&0xFF); superscript = (attr>>12&0x1)==0x1?true:false; log.fine(" " + toString()); this.size = offset-off; this.fullfilled = true; } public Ctrl_AutoNumber(String ctrlId, Node node, int version) throws NotImplementedException { super(ctrlId); NamedNodeMap attributes = node.getAttributes(); numType = NumType.valueOf(attributes.getNamedItem("numType").getNodeValue()); NodeList nodeList = node.getChildNodes(); for (int i=0; i */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; public class Ctrl_Character extends Ctrl { public CtrlCharType ctrlChar; public int charShapeId; public Ctrl_Character(String ctrlId, CtrlCharType ctrlChar) { super(ctrlId); this.ctrlChar = ctrlChar; } public Ctrl_Character(String ctrlId, CtrlCharType ctrlChar, int charShapeId) { super(ctrlId); this.ctrlChar = ctrlChar; this.charShapeId = charShapeId; } @Override public int getSize() { return 1; } public enum CtrlCharType { LINE_BREAK (0x1), PARAGRAPH_BREAK (0x2), HARD_HYPHEN (0x3), HARD_SPACE (0x4); private int type; private CtrlCharType(int type) { this.type = type; } } } H2Orestart-0.7.2/source/HwpDoc/paragraph/Ctrl_Click.java000066400000000000000000000037551476273367000230530ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.nio.charset.StandardCharsets; import java.util.logging.Logger; public class Ctrl_Click extends Ctrl { private static final Logger log = Logger.getLogger(Ctrl_Click.class.getName()); private int size; public String clickHereStr; public Ctrl_Click(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId); int offset = off; offset += 4; offset += 1; short len = (short) ((buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF)*2); offset += 2; if (len > 0) { clickHereStr = new String(buf, offset, len, StandardCharsets.UTF_16LE); offset += len; } offset += 4; offset += 4; this.size = offset-off; log.fine(" " + toString()); } public String toString() { StringBuffer strb = new StringBuffer(); strb.append("CTRL("+ctrlId+")") .append("=문자열:"+(clickHereStr==null?"":clickHereStr)); return strb.toString(); } @Override public int getSize() { return size; } } H2Orestart-0.7.2/source/HwpDoc/paragraph/Ctrl_ColumnDef.java000066400000000000000000000217011476273367000236710ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.NotImplementedException; import HwpDoc.HwpElement.HwpRecordTypes.LineStyle2; public class Ctrl_ColumnDef extends Ctrl { private static final Logger log = Logger.getLogger(Ctrl_ColumnDef.class.getName()); private int size; public int attr; public short colCount; public boolean sameSz; public short sameGap; public short[] colSzWidths; public short[] colSzGaps; public LineStyle2 colLineStyle; public byte colLineWidth; public int colLineColor; public Ctrl_ColumnDef(String ctrlId) { super(ctrlId); } public Ctrl_ColumnDef(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId); int offset = off; // 속성의 bit 0-15(표 139참조) short attrLowBits = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; // 단 사이 간격 sameGap = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; colCount = (short) (attrLowBits>>2 & 0xFF); sameSz = (attrLowBits>>12 & 0x1) == 0x1; // 단 너비가 동일하지 않으면, 단의 개수만큼 단의 폭 if (!sameSz) { colSzWidths = new short[colCount]; colSzGaps = new short[colCount-1]; for(int i=0;i */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; public class Ctrl_Common extends Ctrl { private static final Logger log = Logger.getLogger(Ctrl_Common.class.getName()); private int size; protected int offset; public int objAttr; // 개체 공통 속성 (표 70참조) public boolean treatAsChar; // 글자처럼 취급 여부 public boolean affectLSpacing; // 줄 간격에 영향을 줄지 여부 (TreatAsChar가 true일때만 사용) public VRelTo vertRelTo; // 세로위치기준 (TreatAsChar가 false일때만 사용) public VertAlign vertAlign; // VertRelTo에 대한 상대적인 배열방식. VertRelTo의 값에 따라 가능한 범위가 제한된다. (VertRelTo가 "Para"인 경우 "Para"값만 가능, 나머지 경우에는 모든 값 가능) public HRelTo horzRelTo; // 가로 위치의 기준 (TreatAsChar가 false일때만 사용) public HorzAlign horzAlign; // HorzRelTo에 대한 상대적이 배열방식 public boolean flowWithText; // 오브젝트의 세로 위치를 본문 영역으로 제한할지 여부 (VertRelTo가 Para일때만 사용) public boolean allowOverlap; // 다른 오브젝트와 겹치는 것을 허용할지 여부. (TreatAsChar가 false일대만 사용, flowWithText가 true이면 언제나 false로 간주함) public WidthRelTo widthRelto; // 오브젝트 폭의 기준 public HeightRelTo heightRelto; // 오브젝트 높이의 기준 public TextWrap textWrap; // 0:어울림(Square), 1:자리차지(TopAndBottom),2:글뒤로(BehindText), 3:글앞으로(InFrontOfText) public byte textFlow; // 0:양쪽, 1:왼쪽, 2:오른쪽, 3:큰쪽 public byte numberingType; // 이 객체가 속하는 번호 범위 public int vertOffset; // 세로 오프셋 값 public int horzOffset; // 가로 오프셋 값 public int width; // width 오브젝트의 폭 public int height; // height 오브젝트의 높이 public int zOrder; public short[] outMargin; public int objInstanceID; // 문서 내 각 개체에 대한 고유 아이디(instance ID) public int blockPageBreak; // 쪽나눔 방지 on(1)/off(0) public String objDesc; // 개체 설명문 public List paras; // LIST_HEADER 뒤에 따라오는 PARA_HEADER (복수개) public int captionAttr; // 캡션 속성 public int captionWidth; // 캡션 폭 public int captionSpacing; // 캡션과 틀 사이 간격 public int captionMaxW; // 텍스트의 최대 길이 (=개체의 폭) public List caption; // 캡션이 담길 Paragraph public VertAlign textVerAlign; // (shape)컨트롤 내 문단의 vertical align public Ctrl_Common() { super(); } public Ctrl_Common(String ctrlId) { super(ctrlId); textVerAlign = VertAlign.TOP; } public Ctrl_Common(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId); offset = off; this.ctrlId = ctrlId; objAttr = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; treatAsChar = (objAttr&0x01)==0x01?true:false; affectLSpacing = (objAttr&0x04)==0x04?true:false; vertRelTo = VRelTo.from(objAttr>>3&0x03); vertAlign = VertAlign.from(objAttr>>5&0x07); horzRelTo = HRelTo.from(objAttr>>8&0x03); horzAlign = HorzAlign.from(objAttr>>10&0x07); flowWithText = (objAttr&0x2000)==0x2000?true:false; allowOverlap = (objAttr&0x4000)==0x4000?true:false; widthRelto = WidthRelTo.from(objAttr>>15&0x07); heightRelto = HeightRelTo.from(objAttr>>18&0x03); textWrap = TextWrap.from(objAttr>>21&0x07); textFlow = (byte) (objAttr>>24&0x03); numberingType = (byte) (objAttr>>26&0x07); vertOffset = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; horzOffset = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; width = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; height = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; zOrder = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; outMargin = new short[4]; for (int i=0;i<4;i++) { outMargin[i] = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; } objInstanceID = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; blockPageBreak = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; if (offset-off < size) { int descLen = (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF)*2; offset += 2; if (descLen > 0) { objDesc = new String(buf, offset, descLen, StandardCharsets.UTF_16LE); offset += descLen; } } log.fine(" " + toString()); this.size = offset-off; } public Ctrl_Common(Ctrl_Common common) { super(common.ctrlId); this.objAttr = common.objAttr; this.treatAsChar = common.treatAsChar; this.affectLSpacing = common.affectLSpacing; this.vertRelTo = common.vertRelTo; this.vertAlign = common.vertAlign; this.horzRelTo = common.horzRelTo; this.horzAlign = common.horzAlign; this.flowWithText = common.flowWithText; this.allowOverlap = common.allowOverlap; this.widthRelto = common.widthRelto; this.heightRelto = common.heightRelto; this.textWrap = common.textWrap; this.textFlow = common.textFlow; this.numberingType = common.numberingType; this.vertOffset = common.vertOffset; this.horzOffset = common.horzOffset; this.width = common.width; this.height = common.height; this.zOrder = common.zOrder; this.outMargin = common.outMargin; this.objInstanceID = common.objInstanceID; this.blockPageBreak = common.blockPageBreak; this.objDesc = common.objDesc; this.paras = common.paras; this.captionAttr = common.captionAttr; this.captionWidth = common.captionWidth; this.captionSpacing = common.captionSpacing; this.captionMaxW = common.captionMaxW; this.caption = common.caption; } public Ctrl_Common(String ctrlId, Node node, int version) throws NotImplementedException { this(ctrlId); NamedNodeMap attributes = node.getAttributes(); String numStr = attributes.getNamedItem("id").getNodeValue(); objInstanceID = Integer.parseUnsignedInt(numStr); if (attributes.getNamedItem("pageBreak")!=null) { switch(attributes.getNamedItem("pageBreak").getNodeValue()) { case "TABLE": case "CELL": case "NONE": break; default: if (log.isLoggable(Level.FINE)) { throw new NotImplementedException("Ctrl_Common"); } break; } attributes.removeNamedItem("pageBreak"); } if (attributes.getNamedItem("textFlow")!=null) { switch(attributes.getNamedItem("textFlow").getNodeValue()) { case "BOTH_SIDES": // 0:양쪽, 1:왼쪽, 2:오른쪽, 3:큰쪽 textFlow = 0; break; case "LEFT_ONLY": textFlow = 1; break; case "RIGHT_ONLY": textFlow = 2; break; case "LARGEST_ONLY": textFlow = 3; break; default: if (log.isLoggable(Level.FINE)) { throw new NotImplementedException("Ctrl_Common"); } break; } attributes.removeNamedItem("textFlow"); } if (attributes.getNamedItem("textWrap")!=null) { switch(attributes.getNamedItem("textWrap").getNodeValue()) { case "SQUARE": // bound rect를 따라 textWrap = TextWrap.SQUARE; break; case "TOP_AND_BOTTOM": // 좌, 우에는 텍스트를 배치하지 않음. textWrap = TextWrap.TOP_AND_BOTTOM; break; case "BEHIND_TEXT": // 글과 겹치게 하여 글 뒤로 textWrap = TextWrap.BEHIND_TEXT; break; case "IN_FRONT_OF_TEXT": // 글과 겹치게 하여 글 앞으로 textWrap = TextWrap.IN_FRONT_OF_TEXT; break; default: if (log.isLoggable(Level.FINE)) { throw new NotImplementedException("Ctrl_Common"); } break; } attributes.removeNamedItem("textWrap"); } if (attributes.getNamedItem("zOrder")!=null) { numStr = attributes.getNamedItem("zOrder").getNodeValue(); zOrder = Integer.parseInt(numStr); attributes.removeNamedItem("zOrder"); } if (attributes.getNamedItem("numberingType")!=null) { switch(attributes.getNamedItem("numberingType").getNodeValue()) { case "NONE": numberingType = 0; break; case "PICTURE": numberingType = 1; break; case "TABLE": numberingType = 2; break; case "EQUATION": numberingType = 3; break; default: if (log.isLoggable(Level.FINE)) { throw new NotImplementedException("Ctrl_Common"); } break; } attributes.removeNamedItem("numberingType"); } NodeList nodeList = node.getChildNodes(); for (int i=nodeList.getLength()-1; i>=0; i--) { Node child = nodeList.item(i); switch(child.getNodeName()) { case "hp:sz": { NamedNodeMap childAttrs = child.getAttributes(); numStr = childAttrs.getNamedItem("width").getNodeValue(); width = Integer.parseInt(numStr); widthRelto = WidthRelTo.valueOf(childAttrs.getNamedItem("widthRelTo").getNodeValue()); numStr = childAttrs.getNamedItem("height").getNodeValue(); height = Integer.parseInt(numStr); heightRelto = HeightRelTo.valueOf(childAttrs.getNamedItem("heightRelTo").getNodeValue()); // childAttrs.getNamedItem("protect").getNodeValue(); node.removeChild(child); } break; case "hp:pos": { NamedNodeMap childAttrs = child.getAttributes(); switch(childAttrs.getNamedItem("treatAsChar").getNodeValue()) { case "0": treatAsChar = false; break; case "1": treatAsChar = true; break; default: throw new NotImplementedException("Ctrl_Common"); } if (treatAsChar) { switch(childAttrs.getNamedItem("affectLSpacing").getNodeValue()) { case "0": affectLSpacing = false; break; case "1": affectLSpacing = true; break; default: throw new NotImplementedException("Ctrl_Common"); } } else { switch(childAttrs.getNamedItem("allowOverlap").getNodeValue()) { case "0": allowOverlap = false; break; case "1": allowOverlap = true; break; default: throw new NotImplementedException("Ctrl_Common"); } } if (childAttrs.getNamedItem("vertRelTo")==null) { vertRelTo = VRelTo.PAPER; } else { vertRelTo = VRelTo.valueOf(childAttrs.getNamedItem("vertRelTo").getNodeValue()); } if (childAttrs.getNamedItem("horzRelTo")==null) { horzRelTo = HRelTo.PAPER; } else { horzRelTo = HRelTo.valueOf(childAttrs.getNamedItem("horzRelTo").getNodeValue()); } if (vertRelTo==VRelTo.PARA) { switch(childAttrs.getNamedItem("flowWithText").getNodeValue()) { case "0": flowWithText = false; break; case "1": flowWithText = true; break; default: throw new NotImplementedException("Ctrl_Common"); } } // childAttrs.getNamedItem("holdAnchorAndSO").getNodeValue(); vertAlign = VertAlign.valueOf(childAttrs.getNamedItem("vertAlign").getNodeValue()); horzAlign = HorzAlign.valueOf(childAttrs.getNamedItem("horzAlign").getNodeValue()); numStr = childAttrs.getNamedItem("vertOffset").getNodeValue(); vertOffset = Integer.parseUnsignedInt(numStr); numStr = childAttrs.getNamedItem("horzOffset").getNodeValue(); horzOffset = Integer.parseUnsignedInt(numStr); node.removeChild(child); } break; case "hp:outMargin": { if (outMargin==null) { outMargin = new short[4]; } NamedNodeMap childAttrs = child.getAttributes(); numStr = childAttrs.getNamedItem("left").getNodeValue(); outMargin[0] = (short) Integer.parseInt(numStr); numStr = childAttrs.getNamedItem("right").getNodeValue(); outMargin[1] = (short) Integer.parseInt(numStr); numStr = childAttrs.getNamedItem("top").getNodeValue(); outMargin[2] = (short) Integer.parseInt(numStr); numStr = childAttrs.getNamedItem("bottom").getNodeValue(); outMargin[3] = (short) Integer.parseInt(numStr); node.removeChild(child); } break; case "hp:inMargin": break; case "hp:caption": setCaption(child, version); node.removeChild(child); break; case "hp:shapeComment": break; } } } private void setCaption(Node node, int version) throws NotImplementedException { NamedNodeMap attrs = node.getAttributes(); // [fullSz="0", gap="850", lastWidth="38288", side="TOP", width="8504"] switch(attrs.getNamedItem("side").getNodeValue()) { case "LEFT": captionAttr = 0b00; break; case "RIGHT": captionAttr = 0b01; break; case "TOP": captionAttr = 0b10; break; case "BOTTOM": captionAttr = 0b11; break; default: if (log.isLoggable(Level.FINE)) { throw new NotImplementedException("setCaption"); } break; } switch(attrs.getNamedItem("fullSz").getNodeValue()) { case "0": break; case "1": captionAttr |= 0b100; break; } String numStr = attrs.getNamedItem("width").getNodeValue(); captionWidth = Integer.parseInt(numStr); numStr = attrs.getNamedItem("gap").getNodeValue(); captionSpacing = Integer.parseInt(numStr); numStr = attrs.getNamedItem("lastWidth").getNodeValue(); captionMaxW = Integer.parseInt(numStr); if (caption==null) { caption = new ArrayList<>(); } NodeList nodeList = node.getChildNodes(); for (int i=0; i 0) { obj.objDesc = new String(buf, offset, descLen, StandardCharsets.UTF_16LE); offset += descLen; } if (offset-off < size) { obj.captionAttr = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; obj.captionWidth = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; obj.captionSpacing = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; obj.captionMaxW = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; } return size; } public static int parseCaption(Ctrl_Common obj, int size, byte[] buf, int off, int version) throws HwpParseException { int offset = off; obj.captionAttr = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; obj.captionWidth = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; obj.captionSpacing = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; obj.captionMaxW = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; return offset-off; } public String toString() { StringBuffer strb = new StringBuffer(); strb.append("={객체공통속성:"+Integer.toBinaryString(objAttr)) .append(",세로offset:"+vertOffset) .append(",가로offset:"+horzOffset) .append(",폭:"+width) .append(",높이:"+height) .append(",가로기준:"+horzRelTo.toString()) .append(",세로기준:"+vertRelTo.toString()) .append(",본문배치="+(textWrap.toString())) .append(textWrap!=TextWrap.SQUARE?"":textFlow==0?" 양쪽":textFlow==1?" 왼쪽":textFlow==2?" 오른쪽":textFlow==3?" 큰쪽":""+textFlow) .append(",고유아이디="+objInstanceID) .append(",쪽나눔방지="+blockPageBreak) .append(",개체설명="+objDesc+"}="); return strb.toString(); } @Override public int getSize() { return this.size; } public static enum VRelTo { PAPER (0x0), PAGE (0x1), PARA (0x2); private int num; private VRelTo(int num) { this.num = num; } public static VRelTo from(int num) { for (VRelTo type: values()) { if (type.num == num) return type; } return null; } } public static enum HRelTo { PAPER (0x0), PAGE (0x1), COLUMN (0x2), PARA (0x3); private int num; private HRelTo(int num) { this.num = num; } public static HRelTo from(int num) { for (HRelTo type: values()) { if (type.num == num) return type; } return null; } } public static enum WidthRelTo { PAPER (0x0), PAGE (0x1), COLUMN (0x2), PARA (0x3), ABSOLUTE (0x4); private int num; private WidthRelTo(int num) { this.num = num; } public static WidthRelTo from(int num) { for (WidthRelTo type: values()) { if (type.num == num) return type; } return null; } } public static enum HeightRelTo { PAPER (0x0), PAGE (0x1), ABSOLUTE (0x2); private int num; private HeightRelTo(int num) { this.num = num; } public static HeightRelTo from(int num) { for (HeightRelTo type: values()) { if (type.num == num) return type; } return null; } } public static enum VertAlign { TOP (0x0), CENTER (0x1), BOTTOM (0x2), INSIDE (0x3), OUTSIDE (0x4); private int num; private VertAlign(int num) { this.num = num; } public static VertAlign from(int num) { for (VertAlign type: values()) { if (type.num == num) return type; } return TOP; } } public static enum HorzAlign { LEFT (0x0), CENTER (0x1), RIGHT (0x2), INSIDE (0x3), OUTSIDE (0x4); private int num; private HorzAlign(int num) { this.num = num; } public static HorzAlign from(int num) { for (HorzAlign type: values()) { if (type.num == num) return type; } return LEFT; } } public static enum TextWrap { SQUARE (0x0), TOP_AND_BOTTOM (0x1), BEHIND_TEXT (0x2), IN_FRONT_OF_TEXT (0x3); private int num; private TextWrap(int num) { this.num = num; } public static TextWrap from(int num) { for (TextWrap type: values()) { if (type.num == num) return type; } return SQUARE; } } } H2Orestart-0.7.2/source/HwpDoc/paragraph/Ctrl_Container.java000066400000000000000000000234161476273367000237440ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; public class Ctrl_Container extends Ctrl_GeneralShape { private static final Logger log = Logger.getLogger(Ctrl_Container.class.getName()); private int size; public short nElement; // 개체의 개수 public List ctrlIdList; // 개체의 컨트롤 ID array public List list; // 개체 속성 x 묶음 개체의 갯수 public Ctrl_Container(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId, size, buf, off, version); this.size = offset-off; } public Ctrl_Container(Ctrl_GeneralShape shape) { super(shape); this.size = shape.getSize(); } public Ctrl_Container(String ctrlId, Node node, int version) throws NotImplementedException { super(ctrlId, node, version); NodeList nodeList = node.getChildNodes(); nElement = (short) nodeList.getLength(); if (nElement > 0) { if (list==null) { list = new ArrayList(); } } for (int i=0; i0) { if (obj.ctrlIdList==null) obj.ctrlIdList = new ArrayList(); for (int i=0;i(); for (int i=0;i0) { if (obj.ctrlIdList==null) obj.ctrlIdList = new ArrayList(); for (int i=0;i */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.nio.charset.StandardCharsets; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; public class Ctrl_EqEdit extends Ctrl_GeneralShape { private static final Logger log = Logger.getLogger(Ctrl_EqEdit.class.getName()); private int size; public int attr; // 속성. 스크립트가 차지하는 범위. 첫 비트가 켜져있으면 줄단위, 꺼져있으면 글자 단위 // public short len; // 스크립트 길이 public String eqn; // 한글 수식 스크립트 public int charSize; // 수식 글자 크기 public int color; // 글자 색상 public int baseline; // baseline public String version; // 수식 버전 정보 public String font; // 수식 폰트 이름 public Ctrl_EqEdit(String ctrlId) { super(ctrlId); } public Ctrl_EqEdit(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId, size, buf, off, version); this.size = offset-off; log.fine(" " + toString()); } public Ctrl_EqEdit(Ctrl_GeneralShape shape) { super(shape); this.size = shape.getSize(); } public Ctrl_EqEdit(String ctrlId, Node node, int ver) throws NotImplementedException { super(ctrlId, node, ver); NamedNodeMap attributes = node.getAttributes(); version = attributes.getNamedItem("version").getNodeValue(); String numStr = attributes.getNamedItem("baseLine").getNodeValue(); // 수식이 그려질 기본 선 baseline = Integer.parseInt(numStr); numStr = attributes.getNamedItem("textColor").getNodeValue(); // 수식 글자 색 if (!numStr.equals("NONE")) { numStr = numStr.replaceAll("#", ""); color = Integer.parseInt(numStr, 16); } numStr = attributes.getNamedItem("baseUnit").getNodeValue(); // 수식의 글자 크기 charSize = Integer.parseInt(numStr); switch(attributes.getNamedItem("lineMode").getNodeValue()) { // 수식이 차지하는 범위 case "LINE": attr = 1; break; case "CHAR": attr = 0; break; } font = attributes.getNamedItem("font").getNodeValue(); // 수식 폰트 NodeList nodeList = node.getChildNodes(); for (int i=0; i 0) { len = (short) ((buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF)*2); offset += 2; } if (size-(offset-off) > 0) { obj.version = new String(buf, offset, len, StandardCharsets.UTF_16LE); offset += len; } if (offset-off+2 <= size) { // 5.0.33 버전에서는 이 부분 없음 len = (short) ((buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF)*2); offset += 2; if (offset-off+len <= size) { obj.font = new String(buf, offset, len, StandardCharsets.UTF_16LE); offset += len; } } if (offset-off-size!=0) { log.severe("[CtrlId]=" + obj.ctrlId + ", size=" + size + ", but currentSize=" + (offset-off)); // throw new HwpParseException(); } obj.fullfilled = true; return size; } public static int parseCtrl(Ctrl_EqEdit shape, int size, byte[] buf, int off, int version) throws HwpParseException { int offset = off; int len = Ctrl_Common.parseCtrl(shape, size, buf, offset, version); offset += len; return offset-off; } public static int parseListHeaderAppend(Ctrl_GeneralShape obj, int size, byte[] buf, int off, int version) throws HwpParseException { int offset = off; if (size==24) { offset += 2; obj.captionAttr = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; obj.captionWidth = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; obj.captionSpacing = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; obj.captionMaxW = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; offset += 8; } log.fine(" ctrlID="+obj.ctrlId+", 캡션 parsing이지만, 정확한 parsing은 어떻게 해야 하는지 알 수 없음."); if (offset-off-size!=0) { log.severe("[CtrlID]=" + obj.ctrlId + ", size=" + size + ", but currentSize=" + (offset-off)); // throw new HwpParseException(); } return size; } public String toString() { StringBuffer strb = new StringBuffer(); strb.append("CTRL("+ctrlId+")") .append("=공통속성:"+super.toString()); return strb.toString(); } @Override public int getSize() { return size; } } H2Orestart-0.7.2/source/HwpDoc/paragraph/Ctrl_Form.java000066400000000000000000000027221476273367000227220ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.util.logging.Logger; public class Ctrl_Form extends Ctrl { private static final Logger log = Logger.getLogger(Ctrl_Form.class.getName()); private int size; public Ctrl_Form(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId); this.size = size; log.fine(" CTRL("+ctrlId+")"+" 해석할 수 없습니다."); } @Override public int getSize() { return size; } } H2Orestart-0.7.2/source/HwpDoc/paragraph/Ctrl_GeneralShape.java000066400000000000000000000642041476273367000243600ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.LinkedList; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; import HwpDoc.HwpElement.HwpRecordTypes.LineArrowSize; import HwpDoc.HwpElement.HwpRecordTypes.LineArrowStyle; import HwpDoc.HwpElement.HwpRecordTypes.LineStyle2; import HwpDoc.HwpElement.HwpRecord_BorderFill; import HwpDoc.HwpElement.HwpRecord_BorderFill.Fill; import HwpDoc.paragraph.Ctrl_Character.CtrlCharType; public class Ctrl_GeneralShape extends Ctrl_ObjElement { private static final Logger log = Logger.getLogger(Ctrl_GeneralShape.class.getName()); private HwpParagraph parent; private int size; // 테두리선 정보 public int lineColor; // 선색상 public int lineThick; // 선굵기 // public int lineAttr; // 테두리선 정보 속성 public LineArrowStyle lineHead; public LineArrowStyle lineTail; public LineArrowSize lineHeadSz; public LineArrowSize lineTailSz; public LineStyle2 lineStyle; public byte outline; // Outline style // 채우기 정보 public int fillType; // 채우기 종류 (0:없음, 1:단색, 2:이미지, 4:그라데이션) public Fill fill; // 채우기 // 글상자 텍스트 속성 public short leftSpace; // 글상자 텍스트 왼쪽 여백 public short rightSpace; // 글상자 텍스트 오른쪽 여백 public short upSpace; // 글상자 텍스트 위쪽 여백 public short downSpace; // 글상자 텍스트 아래쪽 여백 public int maxTxtWidth;// 텍스트 문자열 최대 폭 public Ctrl_GeneralShape() { super(); } public Ctrl_GeneralShape(String ctrlId) { super(ctrlId); } public Ctrl_GeneralShape(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId, size, buf, off, version); this.size = offset-off; log.fine(" " + toString()); } public Ctrl_GeneralShape(Ctrl_GeneralShape shape) { super((Ctrl_ObjElement)shape); this.parent = shape.parent; this.lineColor = shape.lineColor; this.lineThick = shape.lineThick; // this.lineAttr = shape.lineAttr; this.lineHead = shape.lineHead; this.lineTail = shape.lineTail; this.lineHeadSz = shape.lineHeadSz; this.lineTailSz = shape.lineTailSz; this.lineStyle = shape.lineStyle; this.outline = shape.outline; this.fillType = shape.fillType; this.fill = shape.fill; this.leftSpace = shape.leftSpace; this.rightSpace = shape.rightSpace; this.upSpace = shape.upSpace; this.downSpace = shape.downSpace; this.maxTxtWidth = shape.maxTxtWidth; } public Ctrl_GeneralShape(String ctrlId, Node node, int version) throws NotImplementedException { super(ctrlId, node, version); // 초기값 lineStyle = LineStyle2.NONE; String numStr; NodeList nodeList = node.getChildNodes(); for (int i=nodeList.getLength()-1; i>=0; i--) { Node child = nodeList.item(i); switch(child.getNodeName()) { case "hp:lineShape": // 그리기 객체의 테두리선 정보 { NamedNodeMap childAttrs = child.getAttributes(); numStr = childAttrs.getNamedItem("color").getNodeValue().replaceAll("#", ""); // 선색상 if (!numStr.equals("none")) { lineColor = Integer.parseInt(numStr, 16); } numStr = childAttrs.getNamedItem("width").getNodeValue(); // 선 굵기 lineThick = Integer.parseUnsignedInt(numStr); lineStyle = LineStyle2.valueOf(childAttrs.getNamedItem("style").getNodeValue()); // 선 종류 /* childAttrs.getNamedItem("endCap").getNodeValue(); // 선 끝 모양 childAttrs.getNamedItem("outlineStyle").getNodeValue(); // 테두리선의 형태 childAttrs.getNamedItem("alpha").getNodeValue(); // 투명도 */ boolean headFill = true; if (childAttrs.getNamedItem("headfill")!=null) { switch(childAttrs.getNamedItem("headfill").getNodeValue()) { case "1": headFill = true; break; case "0": headFill = false; break; } } if (childAttrs.getNamedItem("headStyle")!=null) { switch(childAttrs.getNamedItem("headStyle").getNodeValue()) { // 화살표 시작 모양 case "ARROW": lineHead = LineArrowStyle.ARROW; break; case "SPEAR": lineHead = LineArrowStyle.SPEAR; break; case "CONCAVE_ARROW": lineHead = LineArrowStyle.CONCAVE_ARROW; break; case "EMPTY_DIAMOND": lineHead = headFill?LineArrowStyle.DIAMOND:LineArrowStyle.EMPTY_DIAMOND; break; case "EMPTY_CIRCLE": lineHead = headFill?LineArrowStyle.CIRCLE:LineArrowStyle.EMPTY_CIRCLE; break; case "EMPTY_BOX": lineHead = headFill?LineArrowStyle.BOX:LineArrowStyle.EMPTY_BOX; break; case "NORMAL": default: lineHead = LineArrowStyle.NORMAL; break; } } if (childAttrs.getNamedItem("headSz")!=null) { switch(childAttrs.getNamedItem("headSz").getNodeValue()) { // 화살표 시작 모양 case "SMALL_SMALL": lineHeadSz = LineArrowSize.SMALL_SMALL; break; case "SMALL_MEDIUM": lineHeadSz = LineArrowSize.SMALL_MEDIUM; break; case "SMALL_LARGE": lineHeadSz = LineArrowSize.SMALL_LARGE; break; case "MEDIUM_SMALL": lineHeadSz = LineArrowSize.MEDIUM_SMALL; break; case "MEDIUM_MEDIUM": lineHeadSz = LineArrowSize.MEDIUM_MEDIUM; break; case "MEDIUM_LARGE": lineHeadSz = LineArrowSize.MEDIUM_LARGE; break; case "LARGE_SMALL": lineHeadSz = LineArrowSize.LARGE_SMALL; break; case "LARGE_MEDIUM": lineHeadSz = LineArrowSize.LARGE_MEDIUM; break; case "LARGE_LARGE": lineHeadSz = LineArrowSize.LARGE_LARGE; break; default: lineHeadSz = LineArrowSize.MEDIUM_MEDIUM; break; } } boolean tailFill = true; if (childAttrs.getNamedItem("tailfill")!=null) { switch(childAttrs.getNamedItem("tailfill").getNodeValue()) { case "1": tailFill = true; break; case "0": tailFill = false; break; } } if (childAttrs.getNamedItem("tailStyle")!=null) { switch(childAttrs.getNamedItem("tailStyle").getNodeValue()) { // 화살표 시작 모양 case "ARROW": lineTail = LineArrowStyle.ARROW; break; case "SPEAR": lineTail = LineArrowStyle.SPEAR; break; case "CONCAVE_ARROW": lineTail = LineArrowStyle.CONCAVE_ARROW; break; case "EMPTY_DIAMOND": lineTail = headFill?LineArrowStyle.DIAMOND:LineArrowStyle.EMPTY_DIAMOND; break; case "EMPTY_CIRCLE": lineTail = headFill?LineArrowStyle.CIRCLE:LineArrowStyle.EMPTY_CIRCLE; break; case "EMPTY_BOX": lineTail = headFill?LineArrowStyle.BOX:LineArrowStyle.EMPTY_BOX; break; case "NORMAL": default: lineTail = LineArrowStyle.NORMAL; break; } } if (childAttrs.getNamedItem("tailSz")!=null) { switch(childAttrs.getNamedItem("tailSz").getNodeValue()) { // 화살표 시작 모양 case "SMALL_SMALL": lineTailSz = LineArrowSize.SMALL_SMALL; break; case "SMALL_MEDIUM": lineTailSz = LineArrowSize.SMALL_MEDIUM; break; case "SMALL_LARGE": lineTailSz = LineArrowSize.SMALL_LARGE; break; case "MEDIUM_SMALL": lineTailSz = LineArrowSize.MEDIUM_SMALL; break; case "MEDIUM_MEDIUM": lineTailSz = LineArrowSize.MEDIUM_MEDIUM; break; case "MEDIUM_LARGE": lineTailSz = LineArrowSize.MEDIUM_LARGE; break; case "LARGE_SMALL": lineTailSz = LineArrowSize.LARGE_SMALL; break; case "LARGE_MEDIUM": lineTailSz = LineArrowSize.LARGE_MEDIUM; break; case "LARGE_LARGE": lineTailSz = LineArrowSize.LARGE_LARGE; break; default: lineTailSz = LineArrowSize.MEDIUM_MEDIUM; break; } } node.removeChild(child); } break; case "hc:fillBrush": // 그리기 객체의 채우기 정보 fill = HwpRecord_BorderFill.readFillBrush(child); node.removeChild(child); break; case "hp:drawText": // 그리기 객체 글상자용 텍스트 178 page { NamedNodeMap childAttrs = child.getAttributes(); numStr = childAttrs.getNamedItem("lastWidth").getNodeValue(); // 텍스트 문자열의 최대 폭 if (numStr.matches("\\d+\\.\\d+")) { maxTxtWidth = (int)Double.parseDouble(numStr); } else { maxTxtWidth = (int)Long.parseLong(numStr); } /* childAttrs.getNamedItem("width").getNodeValue(); // 글 상자 이름 childAttrs.getNamedItem("style").getNodeValue()); // 편집 가능 여부 */ NodeList childNodeList = child.getChildNodes(); for (int j=0; j(); } breakP.p.add(new Ctrl_Character(" _", CtrlCharType.PARAGRAPH_BREAK)); paras.add(breakP); } } } public static Ctrl_GeneralShape parse(Ctrl_GeneralShape obj, int size, byte[] buf, int off, int version) throws HwpParseException { int offset = off; // hwp포맷에는 역순으로 ctrlId를 구성한다. 여기서는 순방향으로 구성한다. String ctrlId = new String(buf, offset, 4, StandardCharsets.US_ASCII); offset += 4; Ctrl_GeneralShape shape = null; log.fine(" ctrlID="+ctrlId); // ctrlId를 거꾸로 읽어 비교한다. switch(ctrlId) { case "cip$": // 그림 ShapePic obj = new ShapePic(shape); shape = new Ctrl_ShapePic(obj); offset += Ctrl_ShapePic.parseCtrl((Ctrl_ShapePic)shape, size-(offset-off), buf, offset, version); shape.ctrlId = ctrlId; break; case "cer$": // 사각형 shape = new Ctrl_ShapeRect(obj); offset += Ctrl_ShapeRect.parseCtrl((Ctrl_ShapeRect)shape, size-(offset-off), buf, offset, version); shape.ctrlId = ctrlId; break; case "nil$": // 선 case "loc$": // 개체연결선 shape = new Ctrl_ShapeLine(obj); offset += Ctrl_ShapeLine.parseCtrl((Ctrl_ShapeLine)shape, size-(offset-off), buf, offset, version); shape.ctrlId = ctrlId; break; case "noc$": // 묶음 개체 shape = new Ctrl_Container(obj); offset += Ctrl_Container.parseCtrl((Ctrl_Container)shape, size-(offset-off), buf, offset, version); shape.ctrlId = ctrlId; break; case "lle$": // 타원 shape = new Ctrl_ShapeEllipse(obj); offset += Ctrl_ShapeEllipse.parseCtrl((Ctrl_ShapeEllipse)shape, size-(offset-off), buf, offset, version); shape.ctrlId = ctrlId; break; case "lop$": // 다각형 shape = new Ctrl_ShapePolygon(obj); offset += Ctrl_ShapePolygon.parseCtrl((Ctrl_ShapePolygon)shape, size-(offset-off), buf, offset, version); shape.ctrlId = ctrlId; break; case "cra$": // 호 shape = new Ctrl_ShapeArc(obj); offset += Ctrl_ShapeArc.parseCtrl((Ctrl_ShapeArc)shape, size-(offset-off), buf, offset, version); shape.ctrlId = ctrlId; break; case "ruc$": // 곡선 shape = new Ctrl_ShapeCurve(obj); offset += Ctrl_ShapeCurve.parseCtrl((Ctrl_ShapeCurve)shape, size-(offset-off), buf, offset, version); shape.ctrlId = ctrlId; break; case "deqe": // 한글97 수식 shape = new Ctrl_EqEdit(obj); offset += Ctrl_EqEdit.parseCtrl((Ctrl_EqEdit)shape, size-(offset-off), buf, offset, version); shape.ctrlId = ctrlId; case "elo$": // OLE shape = new Ctrl_ShapeOle(obj); offset += Ctrl_ShapeOle.parseCtrl((Ctrl_ShapeOle)shape, size-(offset-off), buf, offset, version); shape.ctrlId = ctrlId; break; case "div$": // Video shape = new Ctrl_ShapeVideo(obj); offset += Ctrl_ShapeVideo.parseCtrl((Ctrl_ShapeVideo)shape, size-(offset-off), buf, offset, version); shape.ctrlId = ctrlId; break; case "tat$": // TextArt(글맵시) shape = new Ctrl_ShapeTextArt(obj); offset += Ctrl_ShapeTextArt.parseCtrl((Ctrl_ShapeTextArt)shape, size-(offset-off), buf, offset, version); shape.ctrlId = ctrlId; break; default: log.severe("Neither known ctrlID=" + ctrlId+" nor implemented."); break; } if (offset-off-size!=0) { log.fine("[CtrlID]=" + ctrlId + ", size=" + size + ", but currentSize=" + (offset-off)); } return shape; } public static int parseListHeaderAppend(Ctrl_GeneralShape obj, int size, byte[] buf, int off, int version) throws HwpParseException { int offset = off; if (size>=16) { offset += 2; obj.captionAttr = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; obj.captionWidth = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; obj.captionSpacing = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; obj.captionMaxW = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; } if (size-(offset-off)==8) { offset += 8; } if (log.isLoggable(Level.FINE)) { log.fine(" ctrlID="+obj.ctrlId+", 캡션 parsing이지만, 정확한 parsing은 어떻게 해야 하는지 알 수 없음."); } if (offset-off-size!=0) { if (log.isLoggable(Level.SEVERE)) { log.severe("[CtrlID]=" + obj.ctrlId + ", size=" + size + ", but currentSize=" + (offset-off)); } // throw new HwpParseException(); } return size; } public static int parseCtrl(Ctrl_GeneralShape obj, int size, byte[] buf, int off, int version) throws HwpParseException { int offset = off; int len = Ctrl_ObjElement.parseCtrl((Ctrl_ObjElement)obj, size, buf, offset, version); offset += len; obj.lineColor = buf[offset+3]<<24&0xFF000000 | buf[offset]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset+2]&0x000000FF; offset += 4; obj.lineThick = (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; // 문서와 다르게 선 굵기에서 4byte 후에 선 속성이 온다. offset += 2; int lineAttr = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; obj.lineStyle = LineStyle2.from(lineAttr&0x3F); obj.lineHead = LineArrowStyle.from((lineAttr>>10)&0x3F, ((lineAttr>>30)&0x1)==1); obj.lineHeadSz = LineArrowSize.from((lineAttr>>22)&0x0F); obj.lineTail = LineArrowStyle.from((lineAttr>>16)&0x3F, ((lineAttr>>31)&0x1)==1); obj.lineTailSz = LineArrowSize.from((lineAttr>>26)&0x0F); obj.outline = buf[offset++]; obj.fill = new Fill(buf, offset, size-(offset-off)-22); offset += obj.fill.getSize(); // 글상자 텍스트 속성. 아래 내용대로 읽히지 않는다. 알 수 없는 22bytes가 온다. offset += 22; if (log.isLoggable(Level.FINEST)) { log.finest("[그리기 개체 공통 속성]을 읽었습니다."); log.finest("[그리기 개체 글상자용 텍스트 속성]을 읽었습니다. [문단 리스트 헤더]를 읽어야 합니다."); } return offset-off; } public String toString() { StringBuffer strb = new StringBuffer(); strb.append("CTRL("+ctrlId+")") .append("=공통속성:"+super.toString()); return strb.toString(); } public void setParent(HwpParagraph para) { this.parent = para; } public HwpParagraph getParent() { return parent; } @Override public int getSize() { return size; } } H2Orestart-0.7.2/source/HwpDoc/paragraph/Ctrl_HeadFoot.java000066400000000000000000000226451476273367000235160ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; public class Ctrl_HeadFoot extends Ctrl { private static final Logger log = Logger.getLogger(Ctrl_HeadFoot.class.getName()); private int size; public boolean isHeader; public int attr; public PageRange whichPage; public int serialInSec; public int textWidth; public int textHeight; public byte refLevelText; public byte refLevelNum; public List paras; public Ctrl_HeadFoot(String ctrlId) { super(ctrlId); } public Ctrl_HeadFoot(String ctrlId, int size, byte[] buf, int off, int version, boolean isHeader) { super(ctrlId); int offset = off; this.isHeader = isHeader; // 속성(표 141참조) (머리말이 적용 0:양쪽, 1:짝수쪽만, 2:홀수쪽만) attr = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; whichPage = PageRange.from(attr & 0x03); if (whichPage == null) { whichPage = PageRange.BOTH; } serialInSec = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; log.fine(" " + toString()); this.size = offset-off; } public static int parseListHeaderAppend(Ctrl_HeadFoot obj, int size, byte[] buf, int off, int version) throws HwpParseException { int offset = off; offset += 2; // 텍스트 영역의 폭 (1/7200inch로 계산하는가?) obj.textWidth = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; // 텍스트 영역의 높이 (1/7200inch로 계산하는가?) obj.textHeight = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; // 각 비트가 해당 레벨의 텍스트에 대한 참조를 했는지 여부 obj.refLevelText = buf[offset++]; // 각 비트가 해당 레벨의 번호에 대한 참조를 했는지 여부 obj.refLevelNum = buf[offset++]; if (size-(offset-off)>0) { log.fine(" [CtrlId]=" + obj.ctrlId + "," + size + " bytes를 해석하지 못함."); offset += (size-(offset-off)); } obj.fullfilled = true; return offset-off; } public Ctrl_HeadFoot(String ctrlId, Node node, int version) throws NotImplementedException { super(ctrlId); if (ctrlId.equals("daeh")) { isHeader = true; } else { isHeader = false; } NamedNodeMap attributes = node.getAttributes(); // id는 처리하지 않는다. List에 순차적으로 추가한다. // String id = attributes.getNamedItem("id").getNodeValue(); whichPage = PageRange.valueOf(attributes.getNamedItem("applyPageType").getNodeValue()); NodeList nodeList = node.getChildNodes(); for (int i=0; i(); } HwpParagraph p = new HwpParagraph(grandChild, version); paras.add(p); break; default: log.fine(grandChild.getNodeName() + ":" + grandChild.getNodeValue()); if (log.isLoggable(Level.FINE)) { throw new NotImplementedException("Ctrl_HeadFoot"); } break; } } } break; default: log.fine(child.getNodeName() + ":" + child.getNodeValue()); if (log.isLoggable(Level.FINE)) { throw new NotImplementedException("Ctrl_HeadFoot"); } break; } } this.fullfilled = true; } public String toString() { StringBuffer strb = new StringBuffer(); strb.append("CTRL("+ctrlId+")") .append("=속성:"+(whichPage==null?"null":whichPage.toString())) .append(",텍스트폭:"+textWidth) .append(",텍스트높이:"+textHeight) .append(",레벨의텍스트참조여부:"+refLevelText) .append(",레벨의번호참조여부:"+refLevelNum); return strb.toString(); } public int getSize() { return size; } public static enum PageRange { BOTH (0), // 양쪽 EVEN (1), // 짝수쪽 ODD (2); // 홀수쪽 private int range; private PageRange(int range) { this.range = range; } public static PageRange from(int range) { for (PageRange typeNum: values()) { if (typeNum.range == range) return typeNum; } return null; } } } H2Orestart-0.7.2/source/HwpDoc/paragraph/Ctrl_NewNumber.java000066400000000000000000000064551476273367000237300ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.NotImplementedException; import HwpDoc.HwpElement.HwpRecordTypes.NumberShape2; import HwpDoc.paragraph.Ctrl_AutoNumber.NumType; public class Ctrl_NewNumber extends Ctrl { private static final Logger log = Logger.getLogger(Ctrl_NewNumber.class.getName()); private int size; public NumType numType; public NumberShape2 numShape; public short num; public Ctrl_NewNumber(String ctrlId) { super(ctrlId); } public Ctrl_NewNumber(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId); int offset = off; int attr = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; numType = NumType.from(attr&0xF); numShape = NumberShape2.from(attr>>4&0xF); num = (short) (buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF); log.fine(" " + toString()); this.size = offset-off; this.fullfilled = true; } public Ctrl_NewNumber(String ctrlId, Node node, int version) throws NotImplementedException { super(ctrlId); NamedNodeMap attributes = node.getAttributes(); String numStr = attributes.getNamedItem("num").getNodeValue(); num = (short) Integer.parseInt(numStr); numType = NumType.valueOf(attributes.getNamedItem("numType").getNodeValue()); NodeList nodeList = node.getChildNodes(); for (int i=0; i */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.NotImplementedException; public class Ctrl_Note extends Ctrl { private static final Logger log = Logger.getLogger(Ctrl_Note.class.getName()); private int size; public List paras; public Ctrl_Note(String ctrlId) { super(ctrlId); } public Ctrl_Note(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId); int offset = off; // 각주/미주는 문단 리스트 외에 속성을 갖지 않는다. 하지만 쓰레기 값이나 불필요한 업데이트를 줄이기 위해 8 byte를 serialize한다. // 도데체 무슨 말인지??? 8byte를 포함한다는 말인가? offset += 8; log.fine(" " + toString()); this.size = offset-off; this.fullfilled = true; } public Ctrl_Note(String ctrlId, Node node, int version) throws NotImplementedException { super(ctrlId); // id는 처리하지 않는다. List에 순차적으로 추가한다. // String id = attributes.getNamedItem("id").getNodeValue(); NodeList nodeList = node.getChildNodes(); for (int i=0; i */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; public class Ctrl_ObjElement extends Ctrl_Common { private static final Logger log = Logger.getLogger(Ctrl_ObjElement.class.getName()); private int size; public int xGrpOffset; // 개체가 속한 그룹 내에서의 X offset public int yGrpOffset; // 개체가 속한 그룹 내에서의 Y offset public short nGrp; // 몇번이나 그룹 되었는지 public short ver; // 개체 요소의 local file version public int iniWidth; // 개체 생성시 초기 폭 public int iniHeight; // 개체 생성시 초기 높이 public int curWidth; // 개체의 현재 폭 public int curHeight; // 개체의 현재 높이 public boolean horzFlip; // 속성(0:horz flip, 1:ver flip) public boolean verFlip; // 속성(0:horz flip, 1:ver flip) public short rotat; // 회전각 public int xCenter; // 회전 중심의 x 자표 public int yCenter; // 회전 중심의 y 자표 public short matCnt; // scale matrix와 ratation matrix쌍의 갯수. 초기엔 1, group할때마다 하나씩 증가하고, ungroup할때마다 하나씩 감소한다. public double[] matrix; // transalation matrix public double[] matrixSeq; // scale matrix/rotation matrix sequence public Ctrl_ObjElement() { super(); } public Ctrl_ObjElement(String ctrlId) { super(ctrlId); } public Ctrl_ObjElement(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId, size, buf, off, version); this.size = offset-off; log.fine(" " + toString()); } public Ctrl_ObjElement(Ctrl_ObjElement element) { super((Ctrl_Common)element); this.xGrpOffset = element.xGrpOffset; this.yGrpOffset = element.yGrpOffset; this.nGrp = element.nGrp; this.ver = element.ver; this.iniWidth = element.iniWidth; this.iniHeight = element.iniHeight; this.curWidth = element.curWidth; this.curHeight = element.curHeight; this.horzFlip = element.horzFlip; this.verFlip = element.verFlip; this.rotat = element.rotat; this.xCenter = element.xCenter; this.yCenter = element.yCenter; this.matCnt = element.matCnt; this.matrix = element.matrix; this.matrixSeq = element.matrixSeq; } public Ctrl_ObjElement(String ctrlId, Node node, int version) throws NotImplementedException { super(ctrlId, node, version); NamedNodeMap attributes = node.getAttributes(); // attributes.getNamedItem("href").getNodeValue(); // 변경 추적 대상 파일의 경로 // attributes.getNamedItem("InstId").getNodeValue(); String numStr = null; if (attributes.getNamedItem("groupLevel")!=null) { numStr = attributes.getNamedItem("groupLevel").getNodeValue(); nGrp = (short) Integer.parseInt(numStr); matrix = new double[(nGrp+1)*6]; matrixSeq = new double[(nGrp+1)*6*2]; } short scaMatCnt=0, rotMatCnt=0; // scaMatrix 갯수 카운트, rotMatrix 갯수 카운트 NodeList nodeList = node.getChildNodes(); for (int i=nodeList.getLength()-1; i>=0; i--) { Node child = nodeList.item(i); switch(child.getNodeName()) { case "hp:offset": { NamedNodeMap childAttrs = child.getAttributes(); numStr = childAttrs.getNamedItem("x").getNodeValue(); if (numStr.matches("\\d+\\.\\d+")) { xGrpOffset = (int) Double.parseDouble(numStr); } else { xGrpOffset = (int) Long.parseLong(numStr); } numStr = childAttrs.getNamedItem("y").getNodeValue(); if (numStr.matches("\\d+\\.\\d+")) { yGrpOffset = (int) Double.parseDouble(numStr); } else { yGrpOffset = (int) Long.parseLong(numStr); } } node.removeChild(child); break; case "hp:orgSz": { NamedNodeMap childAttrs = child.getAttributes(); numStr = childAttrs.getNamedItem("width").getNodeValue(); if (numStr.matches("\\d+\\.\\d+")) { iniWidth = (int)Double.parseDouble(numStr); } else { iniWidth = Integer.parseInt(numStr); } numStr = childAttrs.getNamedItem("height").getNodeValue(); if (numStr.matches("\\d+\\.\\d+")) { iniHeight = (int)Double.parseDouble(numStr); } else { iniHeight = Integer.parseInt(numStr); } } node.removeChild(child); break; case "hp:curSz": { NamedNodeMap childAttrs = child.getAttributes(); numStr = childAttrs.getNamedItem("width").getNodeValue(); if (numStr.matches("\\d+\\.\\d+")) { curWidth = (int)Double.parseDouble(numStr); } else { curWidth = Integer.parseUnsignedInt(numStr); } numStr = childAttrs.getNamedItem("height").getNodeValue(); if (numStr.matches("\\d+\\.\\d+")) { curHeight = (int)Double.parseDouble(numStr);; } else { curHeight = Integer.parseUnsignedInt(numStr); } } node.removeChild(child); break; case "hp:flip": { NamedNodeMap childAttrs = child.getAttributes(); switch(childAttrs.getNamedItem("horizontal").getNodeValue()) { case "0": horzFlip = false; break; case "1": horzFlip = true; break; } switch(childAttrs.getNamedItem("vertical").getNodeValue()) { case "0": verFlip = false; break; case "1": verFlip = true; break; } } node.removeChild(child); break; case "hp:rotationInfo": { NamedNodeMap childAttrs = child.getAttributes(); if (childAttrs.getNamedItem("angle")!=null) { numStr = childAttrs.getNamedItem("angle").getNodeValue(); rotat = (short) Integer.parseInt(numStr); } if (childAttrs.getNamedItem("centerX")!=null) { numStr = childAttrs.getNamedItem("centerX").getNodeValue(); xCenter = Integer.parseInt(numStr); } if (childAttrs.getNamedItem("centerY")!=null) { numStr = childAttrs.getNamedItem("centerY").getNodeValue(); yCenter = Integer.parseInt(numStr); } // childAttrs.getNamedItem("rotateimage").getNodeValue()) { } node.removeChild(child); break; case "hp:renderingInfo": { NodeList childNodeList = child.getChildNodes(); for (int j=0; j0) { obj.matrixSeq = new double[matrixSize]; for (int i=0;i */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.nio.charset.StandardCharsets; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import HwpDoc.HwpElement.HwpRecordTypes.NumberShape2; public class Ctrl_PageNumPos extends Ctrl { private static final Logger log = Logger.getLogger(Ctrl_PageNumPos.class.getName()); private int size; public NumPos pos; public NumberShape2 numShape; public String userDef; public String prefix; public String postfix; public String constantDash; public Ctrl_PageNumPos(String ctrlId) { super(ctrlId); } public Ctrl_PageNumPos(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId); int offset = off; int attr = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; numShape = NumberShape2.from(attr&0xFF); pos = NumPos.from((attr>>8)&0xF); offset += 2; userDef = new String(buf, offset, 2, StandardCharsets.UTF_16LE); offset += 2; prefix = new String(buf, offset, 2, StandardCharsets.UTF_16LE); offset += 2; postfix = new String(buf, offset, 2, StandardCharsets.UTF_16LE); offset += 2; constantDash = new String(buf, offset, 2, StandardCharsets.UTF_16LE); offset += 2; log.fine(" " + toString()); this.size = offset-off; } public Ctrl_PageNumPos(String ctrlId, Node node, int version) { super(ctrlId); NamedNodeMap attributes = node.getAttributes(); pos = NumPos.valueOf(attributes.getNamedItem("pos").getNodeValue()); numShape = NumberShape2.valueOf(attributes.getNamedItem("formatType").getNodeValue()); // attributes.getNamedItem("sideChar").getNodeValue(); } public String toString() { StringBuffer strb = new StringBuffer(); strb.append("CTRL("+ctrlId+")") .append("=공통속성:"+super.toString()); return strb.toString(); } @Override public int getSize() { return this.size; } public static enum NumPos { NONE (0x0), // 쪽 번호 없음 LEFT_TOP (0x1), // 왼쪽 위 CENTER_TOP (0x2), // 가운데 위 RIGHT_TOP (0x3), // 오른쪽 위 LEFT_BOTTOM (0x4), // 왼쪽 아래 BOTTOM_CENTER (0x5), // 가운데 아래 [20230518] RIGHT_BOTTOM (0x6), // 오른쪽 아래 OUTER_TOP (0x7), // 바깥쪽 위 OUTER_BOTTOM (0x8), // 바깥쪽 아래 INNER_TOP (0x9), // 안쪽 위 INNER_BOTTOM (0x10); // 안쪽 아래 private int num; private NumPos(int num) { this.num = num; } public static NumPos from(int num) { for (NumPos type: values()) { if (type.num == num) return type; } return null; } } } H2Orestart-0.7.2/source/HwpDoc/paragraph/Ctrl_SectionDef.java000066400000000000000000000447001476273367000240440ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import java.util.zip.DataFormatException; import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import HwpDoc.HwpxFile; import HwpDoc.Exception.NotImplementedException; import HwpDoc.section.NoteShape; import HwpDoc.section.Page; import HwpDoc.section.PageBorderFill; import soffice.WriterContext; public class Ctrl_SectionDef extends Ctrl { private static final Logger log = Logger.getLogger(Ctrl_SectionDef.class.getName()); private int size; // 속성 public boolean hideHeader; // 머리말 감추기 여부 public boolean hideFooter; // 꼬리말 감추기 여부 public boolean hideMasterPage; // 바탕쪽 감추기 여부 public boolean hideBorder; // 테두리 감추기 여부 public boolean hideFill; // 배경 감추기 여부 public boolean hidePageNumPos; // 쪽번호 위치 감추기 여부 public boolean showFirstBorder; // 구역의 첫 쪽에서만 테두리 표시여부 public boolean showFirstFill; // 구역의 첫 쪽에만 배경 표시 여부 public byte textDirection; // 텍스트 방향 (0:가로, 1:세로) public boolean hideEmptyLine; // 빈줄 감추기 여부 public byte pageStartOn; // 구역 나눔으로 새 페이지가 생길대의 페이지 번호 적용할지 여부 Both,Even,Odd // public boolean gridPaper; // 원고지 정서법 적용 여부 public short spaceColumns; // 동일한 페이지에서 서로 다른 단 사이의 간격 public short lineGrid; // 세로로 줄맞춤을 할지 여부. 0:off, 1-n:간격을 hwpunit 단위로 지정 public short charGrid; // 가로로 줄맞춤을 할지 여부. 0:off, 1-n:간격을 hwpunit 단위로 지정 public int tabStop; // 기본 탭 간격 (hwpunit or relative characters) public int outlineNumberingID; // 번호 문단 모양 ID public short pageNum; // 쪽 번호 (0:앞 구역에 이어, n= 임의의 번호로 시작) public short figure; // 그림 번호 (0:앞 구역에 이어, n= 임의의 번호로 시작) public short table; // 표 번호 (0:앞 구역에 이어, n= 임의의 번호로 시작) public short equation; // 수식 번호 (0:앞 구역에 이어, n= 임의의 번호로 시작) public short lang; // 대표 language (0:없음, Application에 지정된 language) 5.0.15 이상 public Page page; // HWPTAG_PAGE_DEF 처리 public List headerFooter; // public List noteShapes; // HWPTAG_FOOTNOTE_SHAPE 처리 public List borderFills; // HWPTAG_PAGE_BORDER_FILL 처리 public List paras; // 바탕쪽 public Ctrl_SectionDef(String ctrlId) { super(ctrlId); } public Ctrl_SectionDef(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId); int offset = off; // 속성 (표 130참조) int attr = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; hideHeader = (attr&0x01)==0x01?true:false; hideFooter = (attr&0x02)==0x02?true:false; hideMasterPage = (attr&0x04)==0x04?true:false; hideBorder = (attr&0x08)==0x08?true:false; hideFill = (attr&0x10)==0x10?true:false; hidePageNumPos = (attr&0x20)==0x20?true:false; showFirstBorder = (attr&0x100)==0x100?true:false; showFirstFill = (attr&0x200)==0x200?true:false; textDirection = (byte) (attr>>16&0x07); hideEmptyLine = (attr&0x20000)==0x20000?true:false; pageStartOn = (byte) (attr>>20&0x03); // 동일한 페이지에서 서로 다른 단 사이의 간격. 기본값:1134. 기본설정 11.3pt=4mm=0.158inch spaceColumns = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; // 세로로 줄맞춤을 할지 여부 (0=off, 1-n=간격을 HWPUNIT 단위로 지정) lineGrid = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; // 가로로 줄맞춤을 할지 여부 (0=off, 1-n=간격을 HWPUNIT 단위로 지정) charGrid = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; // 기본 탭 간격 (hwpunit 또는 relative characters) 기본값:8000. 기본설정 40.0pt=14.11mm=0.5556inch tabStop = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; // 번호 문단 모양 ID outlineNumberingID = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; // 쪽 번호 (0=앞 구역에 이어, n=임의의 번호로 시작) pageNum = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; // 그림,표,수식 번호 (0=앞 구역에 이어, n = 임의의 번호로 시작) figure = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; table = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; equation = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; if (version>=5015) { // 대표 language(language값이 없으면(==0), Application에 지정된 language) 5.0.1.5 이상 lang = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; } log.fine(" " + toString(attr)); if (offset-off < size) { } this.size = offset-off; this.fullfilled = true; } public Ctrl_SectionDef(String ctrlId, Node node, int version) throws NotImplementedException { super(ctrlId); NamedNodeMap attributes = node.getAttributes(); // id값은 처리하지 않는다. List에 순차적으로 추가한다. // String id = attributes.getNamedItem("height").getNodeValue(); switch(attributes.getNamedItem("textDirection").getNodeValue()) { case "HORIZONTAL": textDirection = 0; break; // 0:가로, 1:세로 case "VERTICAL": textDirection = 1; break; // 0:가로, 1:세로 default: if (log.isLoggable(Level.FINE)) { throw new NotImplementedException("Ctrl_SectionDef"); } break; } String numStr = attributes.getNamedItem("spaceColumns").getNodeValue(); spaceColumns = (short) Integer.parseInt(numStr); numStr = attributes.getNamedItem("tabStop").getNodeValue(); tabStop = Integer.parseInt(numStr); numStr = attributes.getNamedItem("outlineShapeIDRef").getNodeValue(); outlineNumberingID = Integer.parseInt(numStr); // attributes.getNamedItem("memoShapeIDRef").getNodeValue(); // attributes.getNamedItem("textVerticalWidthHead").getNodeValue(); // attributes.getNamedItem("masterPageCnt").getNodeValue(); NodeList nodeList = node.getChildNodes(); for (int i=0; i(); } NoteShape noteShape = new NoteShape(child, version); noteShapes.add(noteShape); } break; case "hp:pageBorderFill": { if (borderFills==null) { borderFills = new ArrayList(); } PageBorderFill borderFill = new PageBorderFill(child); borderFills.add(borderFill); } break; case "hp:masterPage": // 바탕쪽. hwp에서 읽는 것과 동일한 구조를 갖도록 처리 { NamedNodeMap childAttrs = child.getAttributes(); String pageName = childAttrs.getNamedItem("idRef").getNodeValue(); try { HwpxFile hwpx = WriterContext.getHwpx(); if (hwpx!=null) { Document masterDoc = hwpx.getDocument("Contents/" + pageName + ".xml"); if (masterDoc!=null) { Element element = masterDoc.getDocumentElement(); if (this.paras==null) this.paras = new ArrayList(); NodeList masterNodeList = element.getChildNodes(); for (int masterNodeNum = 0; masterNodeNum < masterNodeList.getLength(); masterNodeNum++) { Node masterNode = masterNodeList.item(masterNodeNum); switch(masterNode.getNodeName()) { case "hp:subList": { // NamedNodeMap masterAttrs = masterNode.getAttributes(); // String attrValue = masterAttrs.getNamedItem("textDirection").getNodeValue(); // attrValue = masterAttrs.getNamedItem("textDirection").getNodeValue(); // attrValue = masterAttrs.getNamedItem("lineWrap").getNodeValue(); // attrValue = masterAttrs.getNamedItem("vertAlign").getNodeValue(); // attrValue = masterAttrs.getNamedItem("linkListIDRef").getNodeValue(); // attrValue = masterAttrs.getNamedItem("textWidth").getNodeValue(); // attrValue = masterAttrs.getNamedItem("textHeight").getNodeValue(); // attrValue = masterAttrs.getNamedItem("hasTextRef").getNodeValue(); // attrValue = masterAttrs.getNamedItem("hasNumRef").getNodeValue(); NodeList subMasterNodeList = masterNode.getChildNodes(); for (int j=0; j */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; import HwpDoc.paragraph.Ctrl_ShapeEllipse.ArcType; public class Ctrl_ShapeArc extends Ctrl_GeneralShape { private static final Logger log = Logger.getLogger(Ctrl_ShapeArc.class.getName()); private int size; // 타원 개체 속성 public ArcType type; // 속성 (표 97참조) public int centerX; // 중심 좌표의 X값 public int centerY; // 중심 좌표의 Y값 public int axixX1; // 제1축 X 좌표의 값 public int axixY1; // 제1축 Y 좌표의 값 public int axixX2; // 제2축 X 좌표의 값 public int axixY2; // 제2축 Y 좌표의 값 public Ctrl_ShapeArc(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId, size, buf, off, version); this.size = offset-off; log.fine(" " + toString()); } public Ctrl_ShapeArc(Ctrl_GeneralShape shape) { super(shape); this.size = shape.getSize(); } public Ctrl_ShapeArc(String ctrlId, Node node, int version) throws NotImplementedException { super(ctrlId, node, version); NamedNodeMap attributes = node.getAttributes(); // 호의 종류 type = ArcType.valueOf(attributes.getNamedItem("type").getNodeValue()); String numStr; NodeList nodeList = node.getChildNodes(); for (int i=0; i */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.NotImplementedException; public class Ctrl_ShapeConnectLine extends Ctrl_GeneralShape { private static final Logger log = Logger.getLogger(Ctrl_ShapeConnectLine.class.getName()); private int size; public ConnectLineType type; // 연결선 형식 public ConnectPoint startPt; // 연결선 시작점 정보 public ConnectPoint endPt; // 연결선 끝점 정보 public Ctrl_ShapeConnectLine(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId, size, buf, off, version); this.size = offset-off; log.fine(" " + toString()); } public Ctrl_ShapeConnectLine(Ctrl_GeneralShape shape) { super(shape); this.size = shape.getSize(); } public Ctrl_ShapeConnectLine(String ctrlId, Node node, int version) throws NotImplementedException { super(ctrlId, node, version); NamedNodeMap attributes = node.getAttributes(); if (attributes.getNamedItem("type")!=null) { type = ConnectLineType.valueOf(attributes.getNamedItem("type").getNodeValue()); } String numStr; NodeList nodeList = node.getChildNodes(); for (int i=0; i */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; public class Ctrl_ShapeCurve extends Ctrl_GeneralShape { private static final Logger log = Logger.getLogger(Ctrl_ShapeCurve.class.getName()); private int size; // 타원 개체 속성 public int nPoints; // count of points public Point[] points; // x,y 좌표 * n public byte[] segmentType; // segment type (0:line, 1:curve) public Ctrl_ShapeCurve(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId, size, buf, off, version); this.size = offset-off; log.fine(" " + toString()); } public Ctrl_ShapeCurve(Ctrl_GeneralShape shape) { super(shape); this.size = shape.getSize(); } public Ctrl_ShapeCurve(String ctrlId, Node node, int version) throws NotImplementedException { super(ctrlId, node, version); String numStr; NodeList nodeList = node.getChildNodes(); points = new Point[nodeList.getLength()]; segmentType = new byte[nodeList.getLength()]; for (int i=0; i 0) { obj.points = new Point[obj.nPoints]; for (int i=0;i1) { obj.segmentType = new byte[obj.nPoints-1]; for (int i=0;i */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.nio.charset.StandardCharsets; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; public class Ctrl_ShapeEllipse extends Ctrl_GeneralShape { private static final Logger log = Logger.getLogger(Ctrl_ShapeEllipse.class.getName()); private int size; // 타원 개체 속성 public boolean intervalDirty; // 속성 (표 97참조) public boolean hasArcProperty; public ArcType arcType; public int centerX; // 중심 좌표의 X값 public int centerY; // 중심 좌표의 Y값 public int axixX1; // 제1축 X 좌표의 값 public int axixY1; // 제1축 Y 좌표의 값 public int axixX2; // 제2축 X 좌표의 값 public int axixY2; // 제2축 Y 좌표의 값 public int startX1; public int startY1; public int endX1; public int endY1; public int startX2; public int startY2; public int endX2; public int endY2; public Ctrl_ShapeEllipse(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId, size, buf, off, version); this.size = offset-off; log.fine(" " + toString()); } public Ctrl_ShapeEllipse(Ctrl_GeneralShape shape) { super(shape); this.size = shape.getSize(); } public Ctrl_ShapeEllipse(String ctrlId, Node node, int version) throws NotImplementedException { super(ctrlId, node, version); NamedNodeMap attributes = node.getAttributes(); // 호로 바뀌었을때 interval을 다시 계산해야 할 필요가 있는지 여부 switch(attributes.getNamedItem("intervalDirty").getNodeValue()) { case "0": intervalDirty = false; break; case "1": intervalDirty = true; break; default: if (log.isLoggable(Level.FINE)) { throw new NotImplementedException("Ctrl_ShapeEllipse"); } }; // 호로 바뀌었는지 여부 switch(attributes.getNamedItem("hasArcPr").getNodeValue()) { case "0": intervalDirty = false; break; case "1": intervalDirty = true; break; default: if (log.isLoggable(Level.FINE)) { throw new NotImplementedException("Ctrl_ShapeEllipse"); } }; // 호의 종류 arcType = ArcType.valueOf(attributes.getNamedItem("arcType").getNodeValue()); String numStr; NodeList nodeList = node.getChildNodes(); for (int i=0; i */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; public class Ctrl_ShapeLine extends Ctrl_GeneralShape { private static final Logger log = Logger.getLogger(Ctrl_ShapeLine.class.getName()); private int size; // 선 개체 속성 public int startX; // 시작점 X 좌표 public int startY; // 시작점 Y 좌표 public int endX; // 끝점 X 좌표 public int endY; // 끝점 Y 좌표 public short attr; // 속성 public Ctrl_ShapeLine(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId, size, buf, off, version); this.size = offset-off; log.fine(" " + toString()); } public Ctrl_ShapeLine(Ctrl_GeneralShape shape) { super(shape); this.size = shape.getSize(); } public Ctrl_ShapeLine(String ctrlId, Node node, int version) throws NotImplementedException { super(ctrlId, node, version); NamedNodeMap attributes = node.getAttributes(); // 처음 생성 시 수직선 또는 수평선일때, 선의 방향이 언제나 오른쪽(위쪽)으로 잡힘으로 인한 현상때문에 방향을 바로 잡아주기 위한 속성 if (attributes.getNamedItem("isReverseHV")!=null) { switch(attributes.getNamedItem("isReverseHV").getNodeValue()) { case "0": attr = 0; break; case "1": attr = 1; break; } } String numStr; NodeList nodeList = node.getChildNodes(); for (int i=nodeList.getLength()-1; i>=0; i--) { Node child = nodeList.item(i); switch(child.getNodeName()) { case "hc:startPt": // 시작점 { NamedNodeMap childAttrs = child.getAttributes(); numStr = childAttrs.getNamedItem("x").getNodeValue(); startX = Integer.parseInt(numStr); numStr = childAttrs.getNamedItem("y").getNodeValue(); startY = Integer.parseInt(numStr); } node.removeChild(child); break; case "hc:endPt": // 끝점 { NamedNodeMap childAttrs = child.getAttributes(); numStr = childAttrs.getNamedItem("x").getNodeValue(); endX = Integer.parseInt(numStr); numStr = childAttrs.getNamedItem("y").getNodeValue(); endY = Integer.parseInt(numStr); } node.removeChild(child); break; } } } public static int parseElement(Ctrl_ShapeLine obj, int size, byte[] buf, int off, int version) throws HwpParseException { int offset = off; if (obj.ctrlId!=null && obj.ctrlId.equals("loc$")) { offset += 4; } obj.startX = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; obj.startY = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; obj.endX = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; obj.endY = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; if (offset-off == size) { return size; } else { obj.attr = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; // 18byte가 아닌 20byte가 온다. 따라서 2byte를 임의로 더해준다. offset += 2; if (offset-off-size!=0) { log.severe("[CtrlId]=" + obj.ctrlId + ", size=" + size + ", but currentSize=" + (offset-off)); // throw new HwpParseException(); } return size; } } public static int parseCtrl(Ctrl_ShapeLine shape, int size, byte[] buf, int off, int version) throws HwpParseException { int offset = off; offset += Ctrl_GeneralShape.parseCtrl(shape, size, buf, offset, version); return offset-off; } public String toString() { StringBuffer strb = new StringBuffer(); strb.append("CTRL("+ctrlId+")") .append("=공통속성:"+super.toString()); return strb.toString(); } @Override public int getSize() { return size; } } H2Orestart-0.7.2/source/HwpDoc/paragraph/Ctrl_ShapeOle.java000066400000000000000000000145221476273367000235200ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; public class Ctrl_ShapeOle extends Ctrl_GeneralShape { private static final Logger log = Logger.getLogger(Ctrl_ShapeOle.class.getName()); private int size; public int attr; // 속성 public int extentX; // 오브젝트 자체의 extent x크기 public int extentY; // 오브젝트 자체의 extent y크기 public String binDataID; // 오브젝트가 사용하는 스토리지의 BinData ID public int borderColor; // 테두리 색 public int borderThick; // 테두리 두께 public int borderAttr; // 테두리 속성 (표 87참조) public Ctrl_ShapeOle(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId, size, buf, off, version); this.size = offset-off; log.fine(" " + toString()); } public Ctrl_ShapeOle(Ctrl_GeneralShape shape) { super(shape); this.size = shape.getSize(); } public Ctrl_ShapeOle(String ctrlId, Node node, int version) throws NotImplementedException { super(ctrlId, node, version); NamedNodeMap attributes = node.getAttributes(); switch(attributes.getNamedItem("objectType").getNodeValue()) { // OLE 객체 종류 case "STATIC": break; default: if (log.isLoggable(Level.FINE)) { throw new NotImplementedException("ShpaeOLE"); } break; } String numStr = attributes.getNamedItem("binaryItemIDRef").getNodeValue(); // OLE 객체 바이너리 데이터에 대한 아이디 참조값 binDataID = numStr; /* switch(attributes.getNamedItem("hasMoniker").getNodeValue()) { // moniker가 설정되어 있는지 여부 case "0": case "1": } attributes.getNamedItem("drawAspect").getNodeValue(); // 화면에 어떤 형태로 표시될지에 대한 설정; "CONTENT" attributes.getNamedItem("dropcapstyle").getNodeValue(); // "None" attributes.getNamedItem("eqBaseLine").getNodeValue(); // 베이스라인; "0" */ NodeList nodeList = node.getChildNodes(); for (int i=0; i */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.HwpFile; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; import HwpDoc.HwpElement.HwpRecord_BinData.Compressed; public class Ctrl_ShapePic extends Ctrl_GeneralShape { private static final Logger log = Logger.getLogger(Ctrl_ShapePic.class.getName()); private int size; public int borderColor; // 테두리 색 public int borderThick; // 테두리 두께 public int borderAttr; // 테두리 속성 public Point[] borderPoints; // 이미지의 테두리 사각형의 x,y 좌표(최초 그림 삽입 시 크기) public int cropLeft; // 자르기 한 후 사각형의 left public int cropTop; // 자르기 한 후 사각형의 top public int cropRight; // 자르기 한 후 사각형의 right public int cropBottom; // 자르기 한 후 사각형의 bottom public short[] innerSpaces; // 안쪽여백(왼쪽여백,오른쪽여백,위쪽여백,아래쪽여백) default:141(표) or 0(그림) public byte bright; // 그림 밝기 public byte contrast; // 그림 명암 public byte effect; // 그림 효과 (0:REAL_PIC,1:GRAY_SCALE,2:BLACK_WHTE,4:PATTERN8x8 public String binDataID; // BinItem의 아이디 참조값 // public ImagePath imagePath; // BinItemID값 대신 문자열을 사용하도록 함 (hwpx); Fill에서 Image 사용할 수 있도록 bindDataID 사용 public byte borderAlpha; // 테두리 투명도 public int instanceID; // 문서 내 각 개체에 대한 고유 아이디(instance ID) public int picEffectInfo; // 그림효과정보(그림자,네온,부드러운,가장자리,반사) public List picEffect; // 각 효과 정보 public int iniPicWidth; // 그림 최초 생성시 기준 이미지 크기 public int iniPicHeight; // 그림 최초 생성시 기준 이미지 크기 public byte picAlpha; // 이미지 투명도 public Ctrl_ShapePic(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId, size, buf, off, version); this.size = offset-off; log.fine(" " + toString()); } public Ctrl_ShapePic(Ctrl_GeneralShape shape) { super(shape); this.size = shape.getSize(); } public Ctrl_ShapePic(String ctrlId, Node node, int version) throws NotImplementedException { super(ctrlId, node, version); NamedNodeMap attributes = node.getAttributes(); // attributes.getNamedItem("reverse").getNodeValue(); String numStr; NodeList nodeList = node.getChildNodes(); for (int i=0; i maxX) || (cropRight < minX || cropRight > maxX) || (cropTop < minY || cropTop > maxY) || (cropBottom < minY || cropBottom > maxY)) { cropLeft = minX; cropRight = maxX; cropTop = minY; cropBottom = maxY; } } break; case "hp:effects": // 이미지 효과 정보 { if (picEffect==null) { picEffect = new ArrayList<>(); } NodeList childNodeList = child.getChildNodes(); for (int j=0; j0 && offset-off(); if ((obj.picEffectInfo&0x1) == 0x1) { PicEffect effect = new Shadow(obj.picEffectInfo, buf, offset, 56); offset += effect.getSize(); obj.picEffect.add(effect); } if ((obj.picEffectInfo&0x2) == 0x2) { PicEffect effect = new Neon(obj.picEffectInfo, buf, offset, 28); offset += effect.getSize(); obj.picEffect.add(effect); } if ((obj.picEffectInfo&0x4) == 0x4) { PicEffect effect = new SoftEdge(obj.picEffectInfo, buf, offset, 4); offset += effect.getSize(); obj.picEffect.add(effect); } if ((obj.picEffectInfo&0x8) == 0x8) { PicEffect effect = new Reflect(obj.picEffectInfo, buf, offset, 56); offset += effect.getSize(); obj.picEffect.add(effect); } } } if (offset-off < size) { // 추가이미지 속성 (그림 최조 생성시 기준 이미지 크기) obj.iniPicWidth = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; // 추가이미지 속성 (그림 최조 생성시 기준 이미지 크기) obj.iniPicHeight = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; if (size-(offset-off)>=1) { // 추가이미지 속성 (이미지 투명도) obj.picAlpha = buf[offset++]; } } log.fine(" " +"(X,Y)=("+obj.xGrpOffset+","+obj.yGrpOffset+")" +",Width="+obj.curWidth+",Height="+obj.curHeight ); if (offset-off-size!=0) { log.severe("[CtrlId]=" + obj.ctrlId + ", size=" + size + ", but currentSize=" + (offset-off)); // 그림 효과를 제대로 읽을 수 없으니 size 맞지 않으니 Exception이 계속 발생할 수 밖에 없다. Exception 발생하지 않도록 처리한다. // throw new HwpParseException(); } return size; } public static int parseCtrl(Ctrl_ShapePic shape, int size, byte[] buf, int off, int version) throws HwpParseException { int offset = off; int len = Ctrl_ObjElement.parseCtrl((Ctrl_ObjElement)shape, size-82, buf, offset, version); offset += len; return offset-off; } public String toString() { StringBuffer strb = new StringBuffer(); strb.append("CTRL("+ctrlId+")") .append("=공통속성:"+super.toString()); return strb.toString(); } @Override public int getSize() { return size; } public static class PicEffect { protected int size; PicEffectType type; public PicEffect(PicEffectType type) { this.type = type; } public PicEffect(int typeNum) { this(PicEffectType.from(typeNum)); } public int getSize() { return this.size; } } public static class Shadow extends PicEffect { public int style; // 그림자 스타일 public int transparency; // 그림자 투명도 public int blur; // 그림자 흐릿하게 public int direction; // 방향 public int distance; // 거리 public float angleX; // 기울기 각도 x public float angleY; // 기울기 각도 y public float magnifyX; // 확배비율 x public float mganifyY; // 확대비율 y public int rotation; // 그림과 함께 그림자 회전 public PicColor color; public Shadow(int typeNum, byte[] buf, int off, int size) throws HwpParseException { super(typeNum); int offset = off; style = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; transparency = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; blur = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; direction = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; distance = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; angleX = ByteBuffer.wrap(buf, offset, 4).order(ByteOrder.LITTLE_ENDIAN).getFloat(); offset += 4; angleY = ByteBuffer.wrap(buf, offset, 4).order(ByteOrder.LITTLE_ENDIAN).getFloat(); offset += 4; magnifyX = ByteBuffer.wrap(buf, offset, 4).order(ByteOrder.LITTLE_ENDIAN).getFloat(); offset += 4; mganifyY = ByteBuffer.wrap(buf, offset, 4).order(ByteOrder.LITTLE_ENDIAN).getFloat(); offset += 4; rotation = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; color = new PicColor(buf, offset, size-(offset-off)); offset += color.getSize(); if (offset-off-size != 0 && offset-off-size+1 != 0) { throw new HwpParseException(); } this.size = offset-off; } public Shadow(PicEffectType type, Node node, int version) { super(type); NamedNodeMap attrs = node.getAttributes(); String numStr = attrs.getNamedItem("style").getNodeValue(); // 그림자스타일 style = Integer.parseInt(numStr); numStr = attrs.getNamedItem("alpha").getNodeValue(); // 시작 투명도 transparency = Integer.parseInt(numStr); numStr = attrs.getNamedItem("radius").getNodeValue(); // 흐릿함 정도 blur = Integer.parseInt(numStr); numStr = attrs.getNamedItem("direction").getNodeValue(); // 방향 각도 direction = Integer.parseInt(numStr); numStr = attrs.getNamedItem("distance").getNodeValue(); // 대상과 그림자 사이의 거리 distance = Integer.parseInt(numStr); // attrs.getNamedItem("alignStyle").getNodeValue(); // 그림자 정렬 switch(attrs.getNamedItem("rotationStyle").getNodeValue()) { // 도형과 함께 그림자 회전 여부 case "0": rotation = 0; break; case "1": rotation = 1; break; } NodeList nodeList = node.getChildNodes(); for (int i=0; i0) { // effectType = new int[nEffect]; // effectValue = new float[nEffect]; // // for (int i=0;i */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.nio.charset.StandardCharsets; import java.util.LinkedList; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; public class Ctrl_ShapePolygon extends Ctrl_GeneralShape { private static final Logger log = Logger.getLogger(Ctrl_ShapePolygon.class.getName()); private int size; // 타원 개체 속성 public int nPoints; // count of points public LinkedList points; // x,y 좌표 * n public Ctrl_ShapePolygon(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId, size, buf, off, version); this.size = offset-off; log.fine(" " + toString()); } public Ctrl_ShapePolygon(Ctrl_GeneralShape shape) { super(shape); this.size = shape.getSize(); } public Ctrl_ShapePolygon(String ctrlId, Node node, int version) throws NotImplementedException { super(ctrlId, node, version); String numStr; NodeList nodeList = node.getChildNodes(); points = new LinkedList(); for (int i=nodeList.getLength()-1; i>=0; i--) { Node child = nodeList.item(i); NamedNodeMap childAttrs = child.getAttributes(); switch(child.getNodeName()) { case "hc:pt": // 다각형 좌표 { Point pt = new Point(); numStr = childAttrs.getNamedItem("x").getNodeValue(); pt.x = Integer.parseInt(numStr); numStr = childAttrs.getNamedItem("y").getNodeValue(); pt.y = Integer.parseInt(numStr); points.add(0,pt); } node.removeChild(child); break; case "#text": break; default: log.fine(child.getNodeName() + "=" + child.getNodeValue()); if (log.isLoggable(Level.FINE)) { throw new NotImplementedException("Ctrl_ShapePolygon"); } break; } } nPoints = points.size(); } public static int parseElement(Ctrl_ShapePolygon obj, int size, byte[] buf, int off, int version) throws HwpParseException { int offset = off; obj.nPoints = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; if (obj.nPoints > 0) { obj.points = new LinkedList(); // [obj.nPoints]; for (int i=0; i */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; public class Ctrl_ShapeRect extends Ctrl_GeneralShape { private static final Logger log = Logger.getLogger(Ctrl_ShapeRect.class.getName()); private int size; // 사각형 개체 속성 public byte curv; // 사각형 모서리 곡률(%) 직각은 0, 둥근모양 20, 반원 50, 그외 % 단위 public Point[] points; // 사각형의 좌표 x, 사각형의 좌표 y public Ctrl_ShapeRect(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId, size, buf, off, version); this.size = offset-off; log.fine(" " + toString()); } public Ctrl_ShapeRect(Ctrl_GeneralShape shape) { super(shape); this.size = shape.getSize(); } public Ctrl_ShapeRect(String ctrlId, Node node, int version) throws NotImplementedException { super(ctrlId, node, version); NamedNodeMap attributes = node.getAttributes(); String numStr = attributes.getNamedItem("ratio").getNodeValue(); curv = (byte) Integer.parseInt(numStr); NodeList nodeList = node.getChildNodes(); if (nodeList!=null && nodeList.getLength()>0) { points = new Point[4]; } for (int i=0; i "{"+String.valueOf(p.x)+","+String.valueOf(p.y)+"}").collect(Collectors.joining(","))+"]" ); if (offset-off-size!=0) { log.severe("[CtrlId]=" + obj.ctrlId + ", size=" + size + ", but currentSize=" + (offset-off)); // throw new HwpParseException(); } return size; } public static int parseListHeaderAppend(Ctrl_ShapeRect obj, int size, byte[] buf, int off, int version) throws HwpParseException { int offset = off; offset += 2; // 글상자 속성 obj.leftSpace = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; obj.rightSpace = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; obj.upSpace = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; obj.downSpace = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; obj.maxTxtWidth = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; if (size-12 <= 13) { // 사이즈가 작을때는 그냥 넘김. return size; } // 알 수 없는 23byte offset += 13; if (size-(offset-off)>0) { offset += 10; // 필드이름 정보 (앞의 23byte 때문에 시작위치가 여기부터인지도 확실하지 않음) int strLen = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; String fieldName= new String(buf, offset, strLen*2, StandardCharsets.UTF_16LE); offset += (strLen*2); log.fine(" [CtrlId]=" + obj.ctrlId + ", fieldName=" + fieldName); offset += (size-(offset-off)); } return offset-off; } public static int parseCtrl(Ctrl_ShapeRect shape, int size, byte[] buf, int off, int version) throws HwpParseException { int offset = off; offset += Ctrl_GeneralShape.parseCtrl(shape, size, buf, offset, version); return offset-off; } public String toString() { StringBuffer strb = new StringBuffer(); strb.append("CTRL("+ctrlId+")") .append("=공통속성:"+super.toString()); return strb.toString(); } @Override public int getSize() { return size; } } H2Orestart-0.7.2/source/HwpDoc/paragraph/Ctrl_ShapeTextArt.java000066400000000000000000000171271476273367000244000ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; public class Ctrl_ShapeTextArt extends Ctrl_GeneralShape { private static final Logger log = Logger.getLogger(Ctrl_ShapeTextArt.class.getName()); private int size; public String text; // 글맵시 내용 public Point pt0; // 첫번째 좌표 public Point pt1; // 두번째 좌표 public Point pt2; // 세번째 좌표 public Point pt3; // 네번째 좌표 // 글맵시 모양 정보 public String fontName; // 글꼴이름 public String fontStyle; // 글꼴 스타일 public String fontType; // 글꼴 형식 public String textShape; // 글맵시 모양 public short lineSpacing; // 줄 간격 public short spacing; // 글자 간격 public String align; // 정렬 방식 public Point[] outline; // 외곽선 정보 public Ctrl_ShapeTextArt(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId, size, buf, off, version); this.size = offset-off; log.fine(" " + toString()); } public Ctrl_ShapeTextArt(Ctrl_GeneralShape shape) { super(shape); this.size = shape.getSize(); } public Ctrl_ShapeTextArt(String ctrlId, Node node, int version) throws NotImplementedException { super(ctrlId, node, version); NamedNodeMap attributes = node.getAttributes(); text = attributes.getNamedItem("text").getNodeValue(); String numStr; NodeList nodeList = node.getChildNodes(); for (int i=0; i */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.nio.charset.StandardCharsets; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; public class Ctrl_ShapeVideo extends Ctrl_GeneralShape { private static final Logger log = Logger.getLogger(Ctrl_ShapeVideo.class.getName()); private int size; public int videoType; // 동영상타입 (0:로컬동영상, 1:웹동영상) public short vidoeBinID; public String webURL; public String thumnailBinID; public Ctrl_ShapeVideo(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId, size, buf, off, version); this.size = offset-off; log.fine(" " + toString()); } public Ctrl_ShapeVideo(Ctrl_GeneralShape shape) { super(shape); this.size = shape.getSize(); } public Ctrl_ShapeVideo(String ctrlId, Node node, int version) throws NotImplementedException { super(ctrlId, node, version); NamedNodeMap attributes = node.getAttributes(); switch(attributes.getNamedItem("type").getNodeValue()) { case "VT_LOCAL": videoType = 0; break; case "VT_WEB": videoType = 1; break; default: if (log.isLoggable(Level.FINE)) { throw new NotImplementedException("Ctrl_ShapeVideo"); } break; } String numStr = attributes.getNamedItem("fileIDRef").getNodeValue(); vidoeBinID = (short) Integer.parseInt(numStr); numStr = attributes.getNamedItem("imageIDRef").getNodeValue(); thumnailBinID = numStr; if (videoType==1) { webURL = attributes.getNamedItem("tag").getNodeValue(); } } public static int parseElement(Ctrl_ShapeVideo obj, int size, byte[] buf, int off, int version) throws HwpParseException { int offset = off; obj.videoType = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; if (obj.videoType==0) { obj.vidoeBinID = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; } else if (obj.videoType==1) { int urlLen = (short) ((buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF)*2); offset += 2; obj.objDesc = new String(buf, offset, urlLen, StandardCharsets.UTF_16LE); offset += urlLen; } short binID = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; obj.thumnailBinID = String.valueOf(binID-1); if (offset-off-size!=0) { log.fine("[CtrlId]=" + obj.ctrlId + ", size=" + size + ", but currentSize=" + (offset-off)); // size 계산 무시 // throw new HwpParseException(); } return size; } public static int parseCtrl(Ctrl_ShapeVideo shape, int size, byte[] buf, int off, int version) throws HwpParseException { int offset = off; int len = Ctrl_ObjElement.parseCtrl(shape, size, buf, offset, version); offset += len; return offset-off; } public String toString() { StringBuffer strb = new StringBuffer(); strb.append("CTRL("+ctrlId+")") .append("=공통속성:"+super.toString()); return strb.toString(); } @Override public int getSize() { return size; } } H2Orestart-0.7.2/source/HwpDoc/paragraph/Ctrl_Table.java000066400000000000000000000307471476273367000230560ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; public class Ctrl_Table extends Ctrl_Common { private static final Logger log = Logger.getLogger(Ctrl_Table.class.getName()); private int size; public int attr; // 표개체 속성의 속성 public short nRows; // RowCount public short nCols; // nCols public short cellSpacing; // CellSpacing public short inLSpace; // 안쪽 왼쪽 여백 public short inRSpace; // 안쪽 오른쪽 여백 public short inUSpace; // 안쪽 위쪽 여백 public short inDSpace; // 안쪽 아래쪽 여백 public short[] rowSize; // Row size public short borderFillID; // Border Fill ID public short validZoneSize; // Valid Zone Info Size(5.0.1.0 이상) public List cellzoneList; // 영역속성 (표 78 참조) (5.0.1.0 이상) public List cells; public Ctrl_Table(String ctrlId) { super(ctrlId); } public Ctrl_Table(String ctrlId, int size, byte[] buf, int off, int version) { super(ctrlId, size, buf, off, version); this.size = offset-off; log.fine(" " + toString()); } public Ctrl_Table(String ctrlId, Node node, int version) throws NotImplementedException { super(ctrlId, node, version); NamedNodeMap attributes = node.getAttributes(); // attributes 중 처리 안된 것들을 알아보기 위해.. 임시로 코드 넣음 List nodeNames = new ArrayList<>(); for (int i=0; i < attributes.getLength(); i++) { nodeNames.add(attributes.item(i).getNodeName()); } //repeatHeader="1" switch(attributes.getNamedItem("repeatHeader").getNodeValue()) { case "0": case "1": break; } nodeNames.remove("repeatHeader"); //rowCnt="12" String numStr = attributes.getNamedItem("rowCnt").getNodeValue(); nRows = (short) Integer.parseInt(numStr); nodeNames.remove("rowCnt"); //noAdjust="1" switch(attributes.getNamedItem("noAdjust").getNodeValue()) { case "0": case "1": break; } nodeNames.remove("noAdjust"); //colCnt="31" numStr = attributes.getNamedItem("colCnt").getNodeValue(); nCols = (short) Integer.parseInt(numStr); nodeNames.remove("colCnt"); //cellSpacing="0" numStr = attributes.getNamedItem("cellSpacing").getNodeValue(); cellSpacing = (short) Integer.parseInt(numStr); nodeNames.remove("cellSpacing"); //borderFillIDRef="3" numStr = attributes.getNamedItem("borderFillIDRef").getNodeValue(); borderFillID = (short) Integer.parseInt(numStr); nodeNames.remove("borderFillIDRef"); //dropcapstyle="None" NodeList nodeList = node.getChildNodes(); for (int i=0; i(); NodeList childNodeList = child.getChildNodes(); for (int j=0; j(); } NodeList childNodeList = child.getChildNodes(); for (int j=0; j=5010 && (offset-off < size)) { table.validZoneSize = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; if (table.validZoneSize>0 && offset-off < size) { table.cellzoneList = new ArrayList(); for (int i=0;i(); log.fine(" " +"Row수="+table.nRows +",Column수="+table.nCols +",RowSize=["+ IntStream.range(0,table.rowSize.length).mapToObj(s-> String.valueOf(table.rowSize[s])).collect(Collectors.joining(",")) +"]" ); if (offset-off-size!=0) { log.fine("[Ctrl]= lbt, size=" + size + ", but currentSize=" + (offset-off)); // dump(buf, off, size); } table.fullfilled = true; return size; } public static int parseListHeaderAppend(Ctrl_Table obj, int size, byte[] buf, int off, int version) throws HwpParseException { int offset = off; if (size==24) { offset += 2; obj.captionAttr = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; obj.captionWidth = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; obj.captionSpacing = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; obj.captionMaxW = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; offset += 8; } // HWP_TABLE 이후 HWP_LIST_HEADER에 붙어 오는 24byte 또는 41byte 를 해석할 수 없다. return size; } public String toString() { StringBuffer strb = new StringBuffer(); strb.append("CTRL("+ctrlId+")") .append("=공통속성:"+super.toString()); return strb.toString(); } @Override public int getSize() { return this.size; } public static class CellZone { short startRowAddr; short startColAddr; short endRowAddr; short endColAddr; short borderFillIDRef; } } H2Orestart-0.7.2/source/HwpDoc/paragraph/HwpParagraph.java000066400000000000000000000376541476273367000234330ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.util.LinkedList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; import HwpDoc.paragraph.Ctrl_Character.CtrlCharType; public class HwpParagraph { private static final Logger log = Logger.getLogger(HwpParagraph.class.getName()); public short paraShapeID; // HWPTAG_PARA_HEADER public short paraStyleID; // HWPTAG_PARA_HEADER public byte breakType; // HWPTAG_PARA_HEADER // public List charShapes; // HWPTAG_PARA_CHAR_SHAPE public LineSeg lineSegs; // HWPTAG_PARA_LINE_SEG public List rangeTags; // HWPTAG_PARA_RANGE_TAG public LinkedList p; // HWPTAG_PARA_TEXT + List V2 public HwpParagraph() { } public HwpParagraph(Node node, int version) throws NotImplementedException { NamedNodeMap attributes = node.getAttributes(); // id값은 처리하지 않는다. List에 순차적으로 추가한다. // String id = attributes.getNamedItem("id").getNodeValue(); String numStr = attributes.getNamedItem("paraPrIDRef").getNodeValue(); paraShapeID = (short) Integer.parseInt(numStr); numStr = attributes.getNamedItem("styleIDRef").getNodeValue(); paraStyleID = (short) Integer.parseInt(numStr); switch(attributes.getNamedItem("pageBreak").getNodeValue()) { case "0": breakType &= 0b11111011; break; // 0:구역나누기, 2:다단나누기, 4:쪽 나누기, 8:단 나누기 case "1": breakType |= 0b00000100; break; default: if (log.isLoggable(Level.FINE)) { throw new NotImplementedException("HwpParagraph"); } break; } switch(attributes.getNamedItem("columnBreak").getNodeValue()) { case "0": breakType &= 0b11110111; break; // 0:구역나누기, 2:다단나누기, 4:쪽 나누기, 8:단 나누기 case "1": breakType |= 0b00001000; break; default: if (log.isLoggable(Level.FINE)) { throw new NotImplementedException("HwpParagraph"); } break; } // attributes.getNamedItem("merged").getNodeValue(); // attributes.getNamedItem("paraTcId").getNodeValue(); int charShapeID = 0; NodeList nodeList = node.getChildNodes(); for (int i=0; i(); p.add(new Ctrl_Character(" _", CtrlCharType.PARAGRAPH_BREAK, charShapeID)); } // 마지막에 PARA_BREAK로 끝나지 않았다면 PARA_BREAK를 삽입 if (p.size()==0 || !(p.getLast() instanceof Ctrl_Character)) { p.add(new Ctrl_Character(" _", CtrlCharType.PARAGRAPH_BREAK, charShapeID)); } } private void parseHwpParagraph(Node node, int charShapeId, int version) throws NotImplementedException { if (p == null) { p = new LinkedList(); } String numStr; Ctrl ctrl; switch(node.getNodeName()) { case "hp:secPr": { ctrl = new Ctrl_SectionDef("dces", node, version); p.add(ctrl); } break; case "hp:ctrl": { NodeList nodeList = node.getChildNodes(); for (int j=0; j=5032 && offset-off */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; public class ParaText extends Ctrl { public String text; public int startIdx; public int charShapeId; public ParaText(String ctrlId, String text, int startIdx) { super(ctrlId); this.text = text; this.startIdx = startIdx; } public ParaText(String ctrlId, String text, int startIdx, int charShapeId) { super(ctrlId); this.text = text; this.startIdx = startIdx; this.charShapeId = charShapeId; } @Override public int getSize() { return text==null?0:text.length(); } } H2Orestart-0.7.2/source/HwpDoc/paragraph/Point.java000066400000000000000000000020671476273367000221260ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; public class Point { public int x; public int y; } H2Orestart-0.7.2/source/HwpDoc/paragraph/RangeTag.java000066400000000000000000000024461476273367000225260ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; public class RangeTag { public int startPos; // 영역 시작 public int endPos; // 영역 끝 public int tag; // 태그(종류 + 데이터): 상위8비트가 종류를 하위 24비트가 종류별로 다른 설명을 부여할 수 있는 임의의 데이터를 나타낸다. } H2Orestart-0.7.2/source/HwpDoc/paragraph/TblCell.java000066400000000000000000000231171476273367000223550ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.paragraph; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.NotImplementedException; import HwpDoc.paragraph.Ctrl_Character.CtrlCharType; import HwpDoc.paragraph.Ctrl_Common.VertAlign; public class TblCell { private static final Logger log = Logger.getLogger(TblCell.class.getName()); private int size; public short colAddr; // 셀 주소(Column, 맨 왼쪽 셀이 0부터 시작하여 1씩 증가) public short rowAddr; // 셀 주소(Row, 맨 위쪽 셀이 0부터 시작하여 1씩 증가) public short colSpan; // 열의 병합 갯수 public short rowSpan; // 행의 병합 갯수 public int width; // 셀의 폭 public int height; // 셀의 높이 public int[] margin; // 셀 4방향 여백 public short borderFill; // 테두리/배경 아이디 public List paras; // 내용 HWPTAG_PARA_TEXT + HWPTAG_PARA_CHAR_SHAPE + HWP_PARA_LINE_SEG public VertAlign verAlign; // LIST_HEADER 에 (문단갯수,텍스트방향,문단줄바꿈,세로정렬) 포함되어 있다. public String mergedColName; // 머지되었을때 칼럼명(알파벳) public TblCell(int size, byte[] buf, int off, int version) { int offset = off; // 앞 2byte는 무시한다. 해석불가. offset += 2; colAddr = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; rowAddr = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; colSpan = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; rowSpan = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; width = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; height = buf[offset+3]<<24&0xFF000000 | buf[offset+2]<<16&0x00FF0000 | buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF; offset += 4; margin = new int[4]; for (int i=0;i<4;i++) { margin[i] = (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; } borderFill = (short) (buf[offset+1]<<8&0xFF00 | buf[offset]&0x00FF); offset += 2; log.fine(" " + "[CELL]" + toString()); // 41byte중 28byte만 해석 가능. 내용을 모르므로 41byte 모두 읽은것 처럼 size 조작한다. this.size = size; } public TblCell(Node node, int version) throws NotImplementedException { NamedNodeMap attrs = node.getAttributes(); // attrs.getNamedItem("name").getNodeValue(); // attrs.getNamedItem("header").getNodeValue(); // attrs.getNamedItem("hasMargin").getNodeValue(); // attrs.getNamedItem("protect").getNodeValue(); // attrs.getNamedItem("editable").getNodeValue(); // attrs.getNamedItem("dirty").getNodeValue(); String numStr = attrs.getNamedItem("borderFillIDRef").getNodeValue(); borderFill = (short)Integer.parseInt(numStr); NodeList nodeList = node.getChildNodes(); for (int i=0; i(); } NodeList childNodeList = child.getChildNodes(); int nodeListNum = childNodeList.getLength(); for (int j=0; j(); } breakP.p.add(new Ctrl_Character(" _", CtrlCharType.PARAGRAPH_BREAK)); paras.add(breakP); } } } break; default: if (log.isLoggable(Level.FINE)) { throw new NotImplementedException("TblCell"); } } } } public String toString() { StringBuffer strb = new StringBuffer(); strb.append("ColAddr="+colAddr) .append(",RowAddr="+rowAddr) .append(",ColSpan="+colSpan) .append(",RowSpan="+rowSpan) .append(",폭="+width) .append(",높이="+height) .append(",테두리/배경 아이디="+borderFill); if (margin!=null) { strb.append(",여백="); for (int i=0; i */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.section; import java.util.logging.Level; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; public class Page { private static final Logger log = Logger.getLogger(Page.class.getName()); public boolean landscape; // 용지 방향 (0:좁게, 1:넓게) public int width; // 용지 가로 크기 (hwpunit) public int height; // 용지 세로 크기 public byte gutterType; // 제책 방법 (LeftOnly,LeftRight,TopBottom) public int marginLeft; // 왼쪽여백 public int marginRight; // 오늘쪽 여백 public int marginTop; // 위 여백 public int marginBottom; // 아래 여백 public int marginHeader; // 머리말 여백 public int marginFooter; // 꼬리말 여백 public int marginGutter; // 제본 여백 public Page() { } public Page(Node node) throws NotImplementedException { NamedNodeMap attributes = node.getAttributes(); switch(attributes.getNamedItem("landscape").getNodeValue()) { case "NARROWLY": landscape = true; break; case "WIDELY": landscape = false; break; // 세로인데, 왜 WIDELY로 되어 있지? default: if (log.isLoggable(Level.FINE)) { throw new NotImplementedException("Page"); } } String numStr = attributes.getNamedItem("width").getNodeValue(); width = Integer.parseInt(numStr); numStr = attributes.getNamedItem("height").getNodeValue(); height = Integer.parseInt(numStr); switch(attributes.getNamedItem("gutterType").getNodeValue()) { case "LEFT_ONELY": gutterType = 0; break; case "LEFT_RIGHT": gutterType = 1; break; case "TOP_BOTTOM": gutterType = 2; break; } NodeList nodeList = node.getChildNodes(); for (int i=0; i>1&0x03); log.fine(" " +"용지=("+page.width+","+page.height+")"+","+(page.landscape?"가로":"세로") +",여백=("+page.marginLeft+","+page.marginRight+","+page.marginTop+","+page.marginBottom+")" +",머리글꼬리글=("+page.marginHeader+","+page.marginFooter+")" ); if (offset-off-size!=0) { throw new HwpParseException(); } return page; } public String toString() { String output = "용지=("+width+","+height+")"+","+(landscape?"가로":"세로") +",여백=("+marginLeft+","+marginRight+","+marginTop+","+marginBottom+")"; return output; } } H2Orestart-0.7.2/source/HwpDoc/section/PageBorderFill.java000066400000000000000000000140471476273367000233560ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package HwpDoc.section; import java.util.logging.Logger; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; public class PageBorderFill { private static final Logger log = Logger.getLogger(PageBorderFill.class.getName()); public boolean textBorder; // 쪽 테두리 위치기준(true:종이기준, false:본문기준) public boolean headerInside; // 머리말 포함 public boolean footerInside; // 꼬리말 포함 public byte fillArea; // 채울영역 Paper, Page, Border public short offsetLeft; // 1417(5mm) public short offsetRight; // 1417(5mm) public short offsetTop; // 1417(5mm) public short offsetBottom; // 1417(5mm) public short borderFill; // BorderFill ID속성값 public PageBorderFill() { } public PageBorderFill(Node node) throws NotImplementedException { NamedNodeMap attributes = node.getAttributes(); switch(attributes.getNamedItem("type").getNodeValue()) { case "BOTH": break; case "EVEN": break; case "ODD": break; } String numStr = attributes.getNamedItem("borderFillIDRef").getNodeValue(); borderFill = (short) Integer.parseInt(numStr); switch(attributes.getNamedItem("textBorder").getNodeValue()) { case "PAPER": textBorder = true; break; case "CONTENT": textBorder = false; break; } switch(attributes.getNamedItem("headerInside").getNodeValue()) { case "0": headerInside = false; break; case "1": headerInside = true; break; } switch(attributes.getNamedItem("footerInside").getNodeValue()) { case "0": footerInside = false; break; case "1": footerInside = true; break; } switch(attributes.getNamedItem("fillArea").getNodeValue()) { case "PAPER": fillArea = 0; break; case "PAGE": fillArea = 1; break; case "BORDER": fillArea = 2; break; } NodeList nodeList = node.getChildNodes(); for (int i=0; i>3&0x03); borderFill.offsetLeft = (short) (buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF); offset += 2; borderFill.offsetRight = (short) (buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF); offset += 2; borderFill.offsetTop = (short) (buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF); offset += 2; borderFill.offsetBottom = (short) (buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF); offset += 2; borderFill.borderFill = (short) (buf[offset+1]<<8&0x0000FF00 | buf[offset]&0x000000FF); offset += 2; log.fine(" " +"배경ID="+borderFill +",테두리간격=("+borderFill.offsetLeft+":"+borderFill.offsetRight+":"+borderFill.offsetTop+":"+borderFill.offsetBottom+")" +",쪽테두리위치="+(borderFill.textBorder?"종이":"본문") +",머리말포함="+(borderFill.headerInside?"Y":"N") +",꼬리말포함="+(borderFill.footerInside?"Y":"N") +",채울영역="+(borderFill.fillArea==0?"Paper":borderFill.fillArea==1?"Page":borderFill.fillArea==2?"Border":"???") ); if (offset-off-size!=0) { throw new HwpParseException(); } return borderFill; } } H2Orestart-0.7.2/source/compare/000077500000000000000000000000001476273367000164625ustar00rootroot00000000000000H2Orestart-0.7.2/source/compare/CompNumbering.java000066400000000000000000000116351476273367000221000ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package compare; import java.util.HashMap; import java.util.Map; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import HwpDoc.HwpElement.HwpRecord_Bullet; import HwpDoc.HwpElement.HwpRecord_Numbering; import HwpDoc.HwpElement.HwpRecord_Numbering.Numbering; import HwpDoc.paragraph.Ctrl_SectionDef; public class CompNumbering { private static final Logger log = Logger.getLogger(CompNumbering.class.getName()); public static Map numberingStyleNameMap = new HashMap(); public static Map bulletStyleNameMap = new HashMap(); private static final String NUMBERING_STYLE_PREFIX = "HWP numbering "; private static final String BULLET_STYLE_PREFIX = "HWP bullet "; // UNOAPI를 사용하지 않는 경우, 현재 numbering 값을 가져오기 위해 사용 private static Map numberingNumbersMap = new HashMap(); private static Map prevNumberingLevelMap = new HashMap(); private static Map bulletNumbersMap = new HashMap(); public static String getNumberingHead(String hwpStyleName, HwpRecord_Numbering numbering, int headingLevel) { Integer[] curNumbers = numberingNumbersMap.get(hwpStyleName); Integer prevLevel = prevNumberingLevelMap.get(hwpStyleName); if (prevLevel == null) { prevLevel = 0; } curNumbers[headingLevel] += 1; StringBuffer sb = new StringBuffer(); // Hwp에서 수준(level)은 7개+확장3개, LibreOffice에서 level은 10개이다. if (headingLevel<7) { Numbering numb = numbering.numbering[headingLevel]; // "" // "^2." // "^2.^3." // "^2.^3.^4" if (numb.numFormat!=null && numb.numFormat.equals("")==false) { Pattern pattern = Pattern.compile("\\^\\d+"); Matcher m = pattern.matcher(numb.numFormat); int prevEnd = 0; while(m.find()) { int s = m.start(); int e = m.end(); int num = Integer.parseInt(numb.numFormat.substring(s+1, e)); if (num <= headingLevel+1) { sb.append(numb.numFormat.subSequence(prevEnd, s)); sb.append(curNumbers[num-1]); } prevEnd = e; } sb.append(numb.numFormat.substring(prevEnd)); } } else { // } // 현재 headingLevel값은 1증가 // curNumbers[headingLevel] += 1; // 하위 headingLevel은 1로 초기화 for (int i=headingLevel+1; i */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package compare; import java.util.HashMap; import java.util.Map; import java.util.logging.Logger; import HwpDoc.paragraph.Ctrl_SectionDef; public class CompPage { private static final Logger log = Logger.getLogger(CompPage.class.getName()); private static Map pageStyleNameMap = new HashMap(); private static Map pageMap = new HashMap(); private static int customIndex = 0; private static int secdIndex = 0; private static final String PAGE_STYLE_PREFIX = "HWP "; public static int getSectionIndex() { return secdIndex; } public static void setSectionIndex(int index) { secdIndex = index; } public static Ctrl_SectionDef getCurrentPage() { return pageMap.get(secdIndex); } public static String makeCustomPageStyle(Ctrl_SectionDef secd) { String styleName = PAGE_STYLE_PREFIX + customIndex; pageStyleNameMap.put(customIndex, styleName); pageMap.put(customIndex, secd); customIndex++; return styleName; } } H2Orestart-0.7.2/source/compare/CompRecurs.java000066400000000000000000000211641476273367000214130ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package compare; import java.util.logging.Logger; import java.util.stream.Collectors; import HwpDoc.paragraph.Ctrl; import HwpDoc.paragraph.Ctrl_Character; import HwpDoc.paragraph.Ctrl_GeneralShape; import HwpDoc.paragraph.Ctrl_Table; import HwpDoc.paragraph.ParaText; import HwpDoc.paragraph.HwpParagraph; public class CompRecurs { private static final Logger log = Logger.getLogger(CompRecurs.class.getName()); private static final String PATTERN_STRING = "[\\u0000\\u000a\\u000d\\u0018-\\u001f]|[\\u0001\\u0002-\\u0009\\u000b-\\u000c\\u000e-\\u0017].{6}[\\u0001\\u0002-\\u0009\\u000b-\\u000c\\u000e-\\u0017]"; private static final String PATTERN_8BYTES = "[\\u0001\\u0002-\\u0009\\u000b-\\u000c\\u000e-\\u0017].{6}[\\u0001\\u0002-\\u0009\\u000b-\\u000c\\u000e-\\u0017]"; public static String getParaString(HwpParagraph para) { StringBuffer sb = new StringBuffer(); if (para.p!=null) { for (Ctrl ctrl : para.p) { if (ctrl==null) continue; switch(ctrl.ctrlId) { case "____": sb.append(((ParaText)ctrl).text); break; case " _": { switch(((Ctrl_Character)ctrl).ctrlChar) { case LINE_BREAK: sb.append("\n"); break; case PARAGRAPH_BREAK: sb.append(""); break; case HARD_HYPHEN: sb.append("-"); break; case HARD_SPACE: sb.append(" "); break; } } break; case "dces": break; case "dloc": break; case "daeh": // 머리말 case "toof": // 꼬리말 break; case " nf": // 각주 case " ne": // 미주 break; case " lbt": // table { // 테이블 경우, 셀 내용을 수집 String tableContent = ((Ctrl_Table) ctrl).cells.stream() .flatMap(cell -> cell.paras.stream()) .filter(cp -> cp.p!=null) .flatMap(cp -> cp.p.stream()) .filter(c -> (c!=null) && (c instanceof ParaText)) .map(c -> (ParaText)c) .map(t -> t.text==null?"":t.text.replaceAll(PATTERN_STRING, "")) .collect(Collectors.joining("|")); sb.append("["+tableContent+"]"); } break; case "onta": // 자동 번호 break; case "onwn": // 새 번호 지정 break; case " osg": // GeneralShapeObject case "cip$": // 그림 ShapePic obj = new ShapePic(shape); case "cer$": // 사각형 case "cra$": // 호 case "elo$": // OLE case "nil$": // 선 case "noc$": // 묶음 개체 case "lle$": // 타원 case "lop$": // 다각형 case "ruc$": // 곡선 case "div$": // 비디오 case "tat$": // 글맵시 { // 그림 개체의 경우 글상자, 캡션만 출력 if (((Ctrl_GeneralShape) ctrl).paras != null) { String content = ((Ctrl_GeneralShape) ctrl).paras.stream() .filter(p -> p.p!=null) .flatMap(p -> p.p.stream()) .filter(c -> c instanceof ParaText) .map(c -> (ParaText)c) .map(t -> t.text.replaceAll(PATTERN_8BYTES, "").replaceAll("[\\u000a\\u000d]", "\\\\n")) .collect(Collectors.joining("")); sb.append("["+content+"]"); } if (((Ctrl_GeneralShape) ctrl).caption != null) { String caption = ((Ctrl_GeneralShape) ctrl).caption.stream() .filter(p -> p.p!=null) .flatMap(p -> p.p.stream()) .filter(c -> c instanceof ParaText) .map(c -> (ParaText)c) .map(cap -> cap.text.replaceAll(PATTERN_STRING, "")) .collect(Collectors.joining("")); sb.append(caption); } } break; case "deqe": // 한글97 수식 break; case "cot%": // FIELD_TABLEOFCONTENT case "klc%": // FIELD_CLICKHERE case "dhgp": // 감추기 case "pngp": // 쪽 번호 위치 case "knu%": // FIELD_UNKNOWN case "etd%": // FIELD_DATE case "tdd%": // FIELD_DOCDATE case "tap%": // FIELD_PATH case "kmb%": // FIELD_BOOKMARK case "gmm%": // FIELD_MAILMERGE case "frx%": // FIELD_CROSSREF case "rms%": // FIELD_SUMMARY case "rsu%": // FIELD_USERINFO case "klh%": // FIELD_HYPERLINK case "gis%": // FIELD_REVISION_SIGN case "d*%%": // FIELD_REVISION_DELETE case "a*%%": // FIELD_REVISION_ATTACH case "C*%%": // FIELD_REVISION_CLIPPING case "S*%%": // FIELD_REVISION_SAWTOOTH case "T*%%": // FIELD_REVISION_THINKING case "P*%%": // FIELD_REVISION_PRAISE case "L*%%": // FIELD_REVISION_LINE case "c*%%": // FIELD_REVISION_SIMPLECHANGE case "h*%%": // FIELD_REVISION_HYPERLINK case "A*%%": // FIELD_REVISION_LINEATTACH case "i*%%": // FIELD_REVISION_LINELINK case "t*%%": // FIELD_REVISION_LINETRANSFER case "r*%%": // FIELD_REVISION_RIGHTMOVE case "l*%%": // FIELD_REVISION_LEFTMOVE case "n*%%": // FIELD_REVISION_TRANSFER case "e*%%": // FIELD_REVISION_SIMPLEINSERT case "lps%": // FIELD_REVISION_SPLIT case "rm%%": // FIELD_REVISION_CHANGE case "em%%": // FIELD_MEMO case "rpc%": // FIELD_PRIVATE_INFO_SECURITY default: } } } return sb.toString(); } } H2Orestart-0.7.2/source/compare/HwpComparer.java000066400000000000000000000322171476273367000215610ustar00rootroot00000000000000package compare; import java.io.IOException; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; import java.util.regex.Pattern; import java.util.zip.DataFormatException; import javax.xml.parsers.ParserConfigurationException; import org.xml.sax.SAXException; import HwpDoc.HwpDetectException; import HwpDoc.HwpDocInfo; import HwpDoc.HwpFile; import HwpDoc.HwpSection; import HwpDoc.HwpxFile; import HwpDoc.Exception.CompoundDetectException; import HwpDoc.Exception.CompoundParseException; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; import HwpDoc.Exception.OwpmlParseException; import HwpDoc.HwpElement.HwpRecord_Bullet; import HwpDoc.HwpElement.HwpRecord_Numbering; import HwpDoc.HwpElement.HwpRecord_ParaShape; import HwpDoc.paragraph.Ctrl_SectionDef; import HwpDoc.paragraph.HwpParagraph; public class HwpComparer { private static final Logger log = Logger.getLogger(HwpComparer.class.getName()); private static final String PATTERN_STRING = "[\\u0000\\u000a\\u000d\\u0018-\\u001f]|[\\u0001\\u0002-\\u0009\\u000b-\\u000c\\u000e-\\u0017].{6}[\\u0001\\u0002-\\u0009\\u000b-\\u000c\\u000e-\\u0017]"; public static Pattern pattern = Pattern.compile(PATTERN_STRING); private static HwpFile hwp = null; private static HwpxFile hwpx = null; public List loadHwp(String inputFile) throws HwpDetectException, CompoundDetectException, NotImplementedException, IOException, CompoundParseException, DataFormatException, HwpParseException, ParserConfigurationException, SAXException, OwpmlParseException { String detectingType = detectHancom(inputFile); if (detectingType==null) { throw new HwpDetectException(); } List sections = null; HwpDocInfo docInfo = null; switch(detectingType) { case "HWP": hwp = new HwpFile(inputFile); hwp.open(); sections = hwp.getSections(); docInfo = hwp.getDocInfo(); break; case "HWPX": hwpx = new HwpxFile(inputFile); hwpx.open(); sections = hwpx.getSections(); docInfo = hwpx.getDocInfo(); break; } for (int i=0; i < docInfo.bulletList.size(); i++) { // Bullet ID는 1부터 시작한다. CompNumbering.makeCustomBulletStyle(i+1, (HwpRecord_Bullet)docInfo.bulletList.get(i)); } for (int i=0; i < docInfo.numberingList.size(); i++) { // Numbering ID는 1부터 시작한다. CompNumbering.makeCustomNumberingStyle(i+1, (HwpRecord_Numbering)docInfo.numberingList.get(i)); } for (HwpSection section: sections) { // 커스톰 PageStyle 생성 Ctrl_SectionDef secd = (Ctrl_SectionDef)section.paraList.stream() .filter(p -> p.p!=null && p.p.size()>0) .flatMap(p -> p.p.stream()) .filter(c -> (c instanceof Ctrl_SectionDef)).findAny().get(); CompPage.makeCustomPageStyle(secd); } // 리턴 자료구조 List paraList = new ArrayList(); int secIndex = 0; for (int i=0; i secd.outlineNumberingID-1) { numberingStyle = (HwpRecord_Numbering)docInfo.numberingList.get(secd.outlineNumberingID-1); numberingPrefix = CompNumbering.getNumberingHead(numberingStyleName, numberingStyle, paraShape.headingLevel); showNumberingPrefix = true; } break; case NUMBER: log.finest("번호문단ID="+paraShape.headingIdRef + ",문단수준="+paraShape.headingLevel); if (paraShape.headingIdRef!=0) { numberingStyleName = CompNumbering.numberingStyleNameMap.get((int)paraShape.headingIdRef); if (numberingStyleName!=null) { numberingStyle = (HwpRecord_Numbering)docInfo.numberingList.get((int)paraShape.headingIdRef-1); numberingPrefix = CompNumbering.getNumberingHead(numberingStyleName, numberingStyle, paraShape.headingLevel); showNumberingPrefix = true; } } break; case BULLET: log.finest("글머리표문단ID="+paraShape.headingIdRef + ",문단수준="+paraShape.headingLevel); if (paraShape.headingIdRef!=0 && docInfo.bulletList.size() > paraShape.headingIdRef-1) { numberingStyleName = CompNumbering.bulletStyleNameMap.get((int)paraShape.headingIdRef); HwpRecord_Bullet bulletStyle = (HwpRecord_Bullet)docInfo.bulletList.get((int)paraShape.headingIdRef-1); numberingPrefix = Character.toString((char)bulletStyle.bulletChar); showNumberingPrefix = true; } break; } String paragraph = CompRecurs.getParaString(para); paraList.add(new ParaNode(numberingPrefix, showNumberingPrefix, paragraph)); } } return paraList; } public void close() { try { if (hwp!=null) hwp.close(); if (hwpx!=null) hwpx.close(); } catch(IOException e) { e.printStackTrace(); } } private static String detectHancom(String inputFile) { String detectingType = null; HwpxFile hwpxTemp = null; try { hwpxTemp = new HwpxFile(inputFile); hwpxTemp.detect(); detectingType = "HWPX"; hwpxTemp.close(); } catch (IOException | HwpDetectException e1) { try { hwpxTemp.close(); } catch (IOException e) { log.severe(e.getMessage()); } HwpFile hwpTemp = null; try { hwpTemp = new HwpFile(inputFile); hwpTemp.detect(); detectingType = "HWP"; hwpTemp.close(); } catch (IOException | HwpDetectException e2) { log.info("file detected neither HWPX nor HWP"); try { hwpTemp.close(); } catch (IOException e) { log.severe(e.getMessage()); } } } return detectingType; } private LinkedList> getMatched(List controlList, List testList) { LinkedList> matches = new LinkedList<>(); for (int i=0; i< controlList.size(); i++) { ParaNode control = controlList.get(i); if (control.content.equals("")) { continue; } if (testList.contains(control)) { // 동일값이 여러개 있다면 가장 처음에 있는것이 찾아진다. matches.add(new Pair<>(control, control)); } } return matches; } public List compare(List controlList, List testList) { List ret = new ArrayList(); LinkedList> matched = getMatched(controlList, testList); int lastIdxA = 0, lastIdxB = 0; for (Pair pair: matched) { ParaNode control = pair.left; ParaNode test = pair.right; int indexA = controlList.indexOf(control); if (indexA < lastIdxA) { for (int i=lastIdxA+1; i<=controlList.lastIndexOf(control); i++) { if (controlList.get(i).equals(control)) { indexA = i; break; } } } // controlList 에서 unmatched List또는 Map을 구성한다. for (int i=lastIdxA+1; i< indexA; i++) { ParaNode node = controlList.get(i); if (node.content.equals("")==false) { System.out.println("[+] "+ (control.showNumberingHead?control.numberingHead:"") + " " + node.content); } } int indexB = testList.indexOf(test); if (indexB < lastIdxB) { for (int i=lastIdxB+1; i<=testList.lastIndexOf(control); i++) { if (testList.get(i).equals(test)) { indexB = i; break; } } } // testList에서 unmatched List 또는 Map을 구성한다. for (int i=lastIdxB+1; i < indexB; i++) { ParaNode node = testList.get(i); if (node.content.equals("")==false) { System.out.println("[-] "+ (control.showNumberingHead?control.numberingHead:"") + " " + node.content); } } System.out.println("[=] "+ (control.showNumberingHead?control.numberingHead:"") + " " + control.content); lastIdxA = indexA; lastIdxB = indexB; } return ret; } private class Pair{ L left; R right; private Pair(L left, R right){ this.left = left; this.right = right; } } public static void main(String[] args) { Logger root = LogManager.getLogManager().getLogger(""); root.setLevel(Level.OFF); for (Handler h : root.getHandlers()) { h.setLevel(Level.OFF); } HwpComparer comp = new HwpComparer(); if (args.length==2 && args[0].equals("-print")) { // Hwp 내용 출력 String inputFile = args[1]; try { List nodes = comp.loadHwp(inputFile); for (ParaNode paragraph: nodes) { if (paragraph.content.equals("")==false) { System.out.println((paragraph.showNumberingHead?String.format("%-5s",paragraph.numberingHead):" ") + paragraph.content); } } } catch (HwpDetectException | CompoundDetectException | NotImplementedException | IOException | CompoundParseException | DataFormatException | HwpParseException | ParserConfigurationException | SAXException | OwpmlParseException e) { e.printStackTrace(); } return; } else if (args.length==3 && args[0].equals("-diff")) { // Hwp 내용 비교 String inputFile1 = args[1]; String inputFile2 = args[2]; try { List compare1 = comp.loadHwp(inputFile1); List compare2 = comp.loadHwp(inputFile2); List compared = comp.compare(compare1, compare2); for (String line: compared) { System.out.println(line); } } catch (HwpDetectException | CompoundDetectException | NotImplementedException | IOException | CompoundParseException | DataFormatException | HwpParseException | ParserConfigurationException | SAXException | OwpmlParseException e) { e.printStackTrace(); } } else { System.out.println("Hwp File compare tool ver 0.1 created by heesu.ban@k2web.co.kr"); System.out.println("Usage #1 (compare hwp files) : java -jar HwpComparer.jar -diff hwpfile1 hwpfile2"); System.out.println("Usage #2 (print hwp content) : java -jar HwpComparer.jar -print hwpfile"); } comp.close(); } } H2Orestart-0.7.2/source/compare/ParaNode.java000066400000000000000000000046151476273367000210240ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package compare; public class ParaNode { public String numberingHead; public boolean showNumberingHead; public String content; public ParaNode(String head, boolean show, String content) { this.numberingHead = head; this.showNumberingHead = show; this.content = content; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((content == null) ? 0 : content.hashCode()); result = prime * result + ((numberingHead == null) ? 0 : numberingHead.hashCode()); result = prime * result + (showNumberingHead ? 1231 : 1237); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; ParaNode other = (ParaNode) obj; if (content == null) { if (other.content != null) return false; } else if (!content.equals(other.content)) return false; if (numberingHead == null) { if (other.numberingHead != null) return false; } else if (!numberingHead.equals(other.numberingHead)) return false; if (showNumberingHead != other.showNumberingHead) return false; return true; } } H2Orestart-0.7.2/source/ebandal/000077500000000000000000000000001476273367000164225ustar00rootroot00000000000000H2Orestart-0.7.2/source/ebandal/libreoffice/000077500000000000000000000000001476273367000206735ustar00rootroot00000000000000H2Orestart-0.7.2/source/ebandal/libreoffice/comp/000077500000000000000000000000001476273367000216315ustar00rootroot00000000000000H2Orestart-0.7.2/source/ebandal/libreoffice/comp/H2OrestartImpl.java000066400000000000000000000501141476273367000253140ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package ebandal.libreoffice.comp; import com.sun.star.uno.Exception; import com.sun.star.uno.UnoRuntime; import com.sun.star.uno.XComponentContext; import com.sun.star.util.CloseVetoException; import com.sun.star.util.XCloseable; import HwpDoc.CustomLogFormatter; import HwpDoc.HwpDetectException; import HwpDoc.HwpSection; import HwpDoc.Exception.CompoundDetectException; import HwpDoc.Exception.CompoundParseException; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; import HwpDoc.Exception.OwpmlParseException; import HwpDoc.HwpElement.HwpRecord_Bullet; import HwpDoc.HwpElement.HwpRecord_CharShape; import HwpDoc.HwpElement.HwpRecord_Numbering; import HwpDoc.HwpElement.HwpRecord_Style; import HwpDoc.paragraph.Ctrl_SectionDef; import HwpDoc.paragraph.HwpParagraph; import soffice.ConvEquation; import soffice.ConvFootnote; import soffice.ConvGraphics; import soffice.ConvNumbering; import soffice.ConvPage; import soffice.ConvPara; import soffice.ConvTable; import soffice.ConvUtil; import soffice.HwpCallback; import soffice.HwpRecurs; import soffice.WriterContext; import com.sun.star.lib.uno.helper.Factory; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.FileTime; import java.nio.file.attribute.PosixFilePermissions; import java.time.Instant; import java.time.ZonedDateTime; import java.util.List; import java.util.Properties; import java.util.Set; import java.util.logging.ConsoleHandler; import java.util.logging.FileHandler; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Stream; import java.util.zip.DataFormatException; import javax.xml.parsers.ParserConfigurationException; import org.xml.sax.SAXException; import com.sun.star.beans.PropertyValue; import com.sun.star.io.XInputStream; import com.sun.star.lang.EventObject; import com.sun.star.lang.IllegalArgumentException; import com.sun.star.lang.XComponent; import com.sun.star.lang.XMultiServiceFactory; import com.sun.star.lang.XSingleComponentFactory; import com.sun.star.registry.XRegistryKey; import com.sun.star.text.XTextDocument; import com.sun.star.lib.uno.helper.WeakBase; import com.sun.star.lib.uno.adapter.XInputStreamToInputStreamAdapter; public final class H2OrestartImpl extends WeakBase implements ebandal.libreoffice.XH2Orestart, com.sun.star.lang.XInitialization, com.sun.star.document.XImporter, com.sun.star.document.XFilter, com.sun.star.document.XExtendedFilterDetection, com.sun.star.util.XCloseListener { private static final Logger log = Logger.getLogger(H2OrestartImpl.class.getName()); private static final String m_implementationName = H2OrestartImpl.class.getName(); /** Service name for the component */ public static final String __serviceName = "ebandal.libreoffice.H2Orestart"; private static final String[] m_serviceNames = { "ebandal.libreoffice.H2Orestart" }; private static WriterContext writerContext; private static String detectedFileExt; private static Logger rootLogger; private static String tmpFilePath; public H2OrestartImpl( XComponentContext context ) { writerContext = new WriterContext(); writerContext.mContext = context; writerContext.mMCF = writerContext.mContext.getServiceManager(); writerContext.userHomeDir = getAppCachePath(); if (rootLogger==null) { cleanTmpFolder(); initialLogger(); } }; public static XSingleComponentFactory __getComponentFactory( String sImplementationName ) { log.fine("__getComponentFactory called"); XSingleComponentFactory xFactory = null; if ( sImplementationName.equals( m_implementationName ) ) xFactory = Factory.createComponentFactory(H2OrestartImpl.class, m_serviceNames); return xFactory; } public static boolean __writeRegistryServiceInfo( XRegistryKey xRegistryKey ) { log.fine("__writeRegistryServiceInfo called"); return Factory.writeRegistryServiceInfo(m_implementationName, m_serviceNames, xRegistryKey); } @Override public void cancel() { log.fine("cancel called"); } @Override public void setTargetDocument(XComponent arg0) throws IllegalArgumentException { log.fine("setTargetDocument called"); writerContext.mMyDocument = UnoRuntime.queryInterface(XTextDocument.class, arg0); writerContext.mMSF = UnoRuntime.queryInterface(XMultiServiceFactory.class, writerContext.mMyDocument); writerContext.mText = writerContext.mMyDocument.getText(); writerContext.mTextCursor = writerContext.mText.createTextCursor(); WriterContext.version = ConvUtil.getVersion(writerContext); } @Override public boolean filter(PropertyValue[] lDescriptor) { log.fine("filter called"); File file = null; String filePath = null; Object inputStream = null; for (int i=0; i sections = writerContext.getSections(); ConvPage.adjustFontIfNotExists(writerContext); // 별 효과 없음. 차라리 미리 font 들을 OS에 설치하는 게 좋겠음. for (int i=0; i < writerContext.getDocInfo().charShapeList.size(); i++) { // Bullet ID는 1부터 시작한다. ConvPara.makeCustomCharacterStyle(writerContext, i+1, (HwpRecord_CharShape)writerContext.getDocInfo().charShapeList.get(i)); } for (int i=0; i < writerContext.getDocInfo().bulletList.size(); i++) { // Bullet ID는 1부터 시작한다. ConvNumbering.makeCustomBulletStyle(writerContext, i+1, (HwpRecord_Bullet)writerContext.getDocInfo().bulletList.get(i)); } for (int i=0; i < writerContext.getDocInfo().numberingList.size(); i++) { // Numbering ID는 1부터 시작한다. ConvNumbering.makeCustomNumberingStyle(writerContext, i+1, (HwpRecord_Numbering)writerContext.getDocInfo().numberingList.get(i)); } for (HwpSection section: sections) { // 커스톰 PageStyle 생성 Ctrl_SectionDef secd = (Ctrl_SectionDef)section.paraList.stream() .filter(p -> p.p!=null && p.p.size()>0) .flatMap(p -> p.p.stream()) .filter(c -> (c instanceof Ctrl_SectionDef)).findAny().get(); ConvPage.makeCustomPageStyle(writerContext, secd); } for (int i=0; i attrViews = baseDir.getFileSystem().supportedFileAttributeViews(); if (attrViews.contains("posix")) { if (baseDir.toFile().exists()) { Files.setPosixFilePermissions(baseDir, PosixFilePermissions.fromString("rwx------")); } else { Files.createDirectories(baseDir, PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"))); } } else { Files.createDirectories(baseDir); } writerContext.userHomeDir = baseDir; // "%h" the value of the "user.home" system property FileHandler fileHandler = new FileHandler(baseDir.toAbsolutePath() + "/import_%g.log", 4194304, 10, false); fileHandler.setLevel(Level.INFO); CustomLogFormatter sformatter = new CustomLogFormatter(); fileHandler.setFormatter(sformatter); rootLogger.addHandler(fileHandler); } catch (IOException e) { e.printStackTrace(); } } private void cleanTmpFolder() { Path tmpFolder = getAppCachePath(); if (tmpFolder.toFile().exists()) { try (Stream paths = Files.find(tmpFolder, Integer.MAX_VALUE, (path, attr) -> { Instant delInstant = ZonedDateTime.now().minusDays(5).toInstant(); FileTime fileTime = FileTime.from(delInstant); int comp = attr.creationTime().compareTo(fileTime); if (path.toFile().isFile() && comp==-1) { return true; } else { return false; } })) { paths.forEach(p -> { try { Files.deleteIfExists(p); } catch (IOException e) { e.printStackTrace(); } }); } catch (IOException e) { e.printStackTrace(); } } } private void reset() { log.fine("Resetting Page info."); ConvPage.reset(writerContext); log.fine("Resetting Numbering info."); ConvNumbering.reset(writerContext); log.fine("Resetting Paragraph info."); ConvPara.reset(writerContext); log.fine("Resetting Equasion info."); ConvEquation.reset(writerContext); log.fine("Resetting Graphics info."); ConvGraphics.reset(writerContext); log.fine("Resetting Table info."); ConvTable.reset(writerContext); log.fine("Resetting Footnote info."); ConvFootnote.reset(writerContext); if (writerContext!=null) { log.fine("HwpFile still exists. Will be closed."); try { writerContext.close(); } catch (IOException | HwpDetectException e) { log.severe(e.getMessage()); } } else { log.fine("HwpFile not exists."); } } private String copyToTmpFile(Object inputStream) { String ret = null; byte[] buf = new byte[4096]; XInputStream xinput = UnoRuntime.queryInterface(XInputStream.class, inputStream); try { Path baseDir = getAppCachePath(); Set attrViews = baseDir.getFileSystem().supportedFileAttributeViews(); File tmpFile = null; if (attrViews.contains("posix")) { tmpFile = Files.createTempFile(baseDir, "H2O_TMP_", null, PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------"))) .toFile(); } else { tmpFile = Files.createTempFile(baseDir, "H2O_TMP_", null) .toFile(); } ret = tmpFile.toString(); try (FileOutputStream fos = new FileOutputStream(tmpFile); XInputStreamToInputStreamAdapter adapter = new XInputStreamToInputStreamAdapter(xinput)) { while(true) { int readLen = adapter.read(buf, 0, buf.length); fos.write(buf, 0, readLen); if (readLen != buf.length) { break; } } } xinput.closeInput(); } catch (IOException | com.sun.star.io.IOException e) { e.printStackTrace(); } return ret; } private Path getAppCachePath() { String osName = System.getProperty("os.name").toLowerCase(); if (osName.contains("linux")) { // Linux: Use XDG Base Directory Specification // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html String cacheHomeDir = System.getenv("XDG_CACHE_HOME"); if (cacheHomeDir == null || cacheHomeDir.isEmpty()) return Paths.get(System.getProperty("user.home"), ".cache", "H2Orestart"); else return Paths.get(cacheHomeDir, "H2Orestart"); } else if (osName.contains("mac")) { // MacOS: Use ~/Library/Caches/H2Orestart // https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html return Paths.get(System.getProperty("user.home"), "Library", "Caches", "H2Orestart"); } else { return Paths.get(System.getProperty("user.home"),".H2Orestart"); } } } H2Orestart-0.7.2/source/ebandal/libreoffice/comp/RegistrationHandler.classes000066400000000000000000000000501476273367000271530ustar00rootroot00000000000000ebandal.libreoffice.comp.H2OrestartImpl H2Orestart-0.7.2/source/ebandal/libreoffice/comp/RegistrationHandler.java000066400000000000000000000135171476273367000264530ustar00rootroot00000000000000/************************************************************************* * * The Contents of this file are made available subject to the terms of * either of the GNU Lesser General Public License Version 2.1 * * Sun Microsystems Inc., October, 2000 * * * GNU Lesser General Public License Version 2.1 * ============================================= * Copyright 2000 by Sun Microsystems, Inc. * 901 San Antonio Road, Palo Alto, CA 94303, USA * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License version 2.1, as published by the Free Software Foundation. * * 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 * * The Initial Developer of the Original Code is: Sun Microsystems, Inc.. * * Copyright: 2002 by Sun Microsystems, Inc. * * All Rights Reserved. * * Contributor(s): Cedric Bosdonnat * * ************************************************************************/ package ebandal.libreoffice.comp; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.lang.reflect.Method; import java.util.ArrayList; import com.sun.star.lang.XSingleComponentFactory; import com.sun.star.registry.XRegistryKey; /** * Component main registration class. * *

This class should not be modified.

* * @author Cedric Bosdonnat aka. cedricbosdo * */ public class RegistrationHandler { /** * Get a component factory for the implementations handled by this class. * *

This method calls all the methods of the same name from the classes listed * in the RegistrationHandler.classes file. This method * should not be modified.

* * @param pImplementationName the name of the implementation to create. * * @return the factory which can create the implementation. */ public static XSingleComponentFactory __getComponentFactory(String sImplementationName ) { XSingleComponentFactory xFactory = null; Class[] classes = findServicesImplementationClasses(); int i = 0; while (i < classes.length && xFactory == null) { Class clazz = classes[i]; if ( sImplementationName.equals( clazz.getCanonicalName() ) ) { try { Class[] getTypes = new Class[]{String.class}; Method getFactoryMethod = clazz.getMethod("__getComponentFactory", getTypes); Object o = getFactoryMethod.invoke(null, sImplementationName); xFactory = (XSingleComponentFactory)o; } catch (Exception e) { // Nothing to do: skip System.err.println("Error happened"); e.printStackTrace(); } } i++; } return xFactory; } /** * Writes the services implementation informations to the UNO registry. * *

This method calls all the methods of the same name from the classes listed * in the RegistrationHandler.classes file. This method * should not be modified.

* * @param pRegistryKey the root registry key where to write the informations. * * @return true if the informations have been successfully written * to the registry key, false otherwise. */ public static boolean __writeRegistryServiceInfo(XRegistryKey xRegistryKey ) { Class[] classes = findServicesImplementationClasses(); boolean success = true; int i = 0; while (i < classes.length && success) { Class clazz = classes[i]; try { Class[] writeTypes = new Class[]{XRegistryKey.class}; Method getFactoryMethod = clazz.getMethod("__writeRegistryServiceInfo", writeTypes); Object o = getFactoryMethod.invoke(null, xRegistryKey); success = success && ((Boolean)o).booleanValue(); } catch (Exception e) { success = false; e.printStackTrace(); } i++; } return success; } /** * @return all the UNO implementation classes. */ private static Class[] findServicesImplementationClasses() { ArrayList classes = new ArrayList(); InputStream in = RegistrationHandler.class.getResourceAsStream("RegistrationHandler.classes"); LineNumberReader reader = new LineNumberReader(new InputStreamReader(in)); try { String line = reader.readLine(); while (line != null) { if (!line.equals("")) { line = line.trim(); try { Class clazz = Class.forName(line); Class[] writeTypes = new Class[]{XRegistryKey.class}; Class[] getTypes = new Class[]{String.class}; Method writeRegMethod = clazz.getMethod("__writeRegistryServiceInfo", writeTypes); Method getFactoryMethod = clazz.getMethod("__getComponentFactory", getTypes); if (writeRegMethod != null && getFactoryMethod != null) { classes.add(clazz); } } catch (Exception e) { e.printStackTrace(); } } line = reader.readLine(); } } catch (IOException e) { e.printStackTrace(); } finally { try { reader.close(); in.close(); } catch (Exception e) {}; } return classes.toArray(new Class[classes.size()]); } } H2Orestart-0.7.2/source/ebandal/libreoffice/comp/tests/000077500000000000000000000000001476273367000227735ustar00rootroot00000000000000H2Orestart-0.7.2/source/ebandal/libreoffice/comp/tests/UnoTests.java000066400000000000000000000004721476273367000254250ustar00rootroot00000000000000package ebandal.libreoffice.comp.tests; import org.junit.runner.RunWith; import org.junit.runners.Suite.SuiteClasses; import ebandal.libreoffice.comp.tests.base.UnoSuite; import ebandal.libreoffice.comp.tests.uno.WriterTest; @RunWith(UnoSuite.class) @SuiteClasses({WriterTest.class}) public class UnoTests { } H2Orestart-0.7.2/source/ebandal/libreoffice/comp/tests/base/000077500000000000000000000000001476273367000237055ustar00rootroot00000000000000H2Orestart-0.7.2/source/ebandal/libreoffice/comp/tests/base/UnoSuite.java000066400000000000000000000044131476273367000263250ustar00rootroot00000000000000package ebandal.libreoffice.comp.tests.base; import java.util.List; import org.junit.runner.Runner; import org.junit.runner.notification.RunNotifier; import org.junit.runners.Suite; import org.junit.runners.model.InitializationError; import org.junit.runners.model.RunnerBuilder; import com.sun.star.frame.XDesktop; import com.sun.star.lang.XMultiComponentFactory; import com.sun.star.uno.UnoRuntime; import com.sun.star.uno.XComponentContext; public class UnoSuite extends Suite { private static XComponentContext componentContext; public UnoSuite(Class klass, RunnerBuilder builder) throws InitializationError { super(klass, builder); } public UnoSuite(RunnerBuilder builder, Class[] classes) throws InitializationError { super(builder, classes); } public UnoSuite(Class klass, Class[] suiteClasses) throws InitializationError { super(klass, suiteClasses); } public UnoSuite(Class klass, List runners) throws InitializationError { super(klass, runners); } public UnoSuite(RunnerBuilder builder, Class klass, Class[] suiteClasses) throws InitializationError { super(builder, klass, suiteClasses); } @Override public void run(RunNotifier arg0) { try { startOffice(); } catch (Exception e) { e.printStackTrace(); } super.run(arg0); stopOffice(); } private void startOffice() throws Exception { componentContext = com.sun.star.comp.helper.Bootstrap.bootstrap(); } private void stopOffice() { try { if (componentContext != null) { // Only the uno test suite which started the office can stop it XMultiComponentFactory xMngr = componentContext.getServiceManager(); Object oDesktop = xMngr.createInstanceWithContext("com.sun.star.frame.Desktop", componentContext); XDesktop xDesktop = (XDesktop)UnoRuntime.queryInterface(XDesktop.class, oDesktop); xDesktop.terminate(); } } catch (Exception e) { e.printStackTrace(); } } public static XComponentContext getComponentContext() { return componentContext; } } H2Orestart-0.7.2/source/ebandal/libreoffice/comp/tests/helper/000077500000000000000000000000001476273367000242525ustar00rootroot00000000000000H2Orestart-0.7.2/source/ebandal/libreoffice/comp/tests/helper/UnoHelper.java000066400000000000000000000021221476273367000270130ustar00rootroot00000000000000package ebandal.libreoffice.comp.tests.helper; import com.sun.star.beans.PropertyValue; import com.sun.star.frame.FrameSearchFlag; import com.sun.star.frame.XComponentLoader; import com.sun.star.lang.XComponent; import com.sun.star.lang.XMultiComponentFactory; import com.sun.star.text.XTextDocument; import com.sun.star.uno.Exception; import com.sun.star.uno.UnoRuntime; import ebandal.libreoffice.comp.tests.base.UnoSuite; public class UnoHelper { public static XTextDocument getWriterDocument() throws Exception { XMultiComponentFactory xMngr = UnoSuite.getComponentContext().getServiceManager(); Object oDesktop = xMngr.createInstanceWithContext("com.sun.star.frame.Desktop", UnoSuite.getComponentContext()); XComponentLoader xLoader = (XComponentLoader)UnoRuntime.queryInterface( XComponentLoader.class, oDesktop); XComponent xDoc = xLoader.loadComponentFromURL("private:factory/swriter", "_default", FrameSearchFlag.ALL, new PropertyValue[0]); return (XTextDocument)UnoRuntime.queryInterface(XTextDocument.class, xDoc); } } H2Orestart-0.7.2/source/ebandal/libreoffice/comp/tests/uno/000077500000000000000000000000001476273367000235745ustar00rootroot00000000000000H2Orestart-0.7.2/source/ebandal/libreoffice/comp/tests/uno/WriterTest.java000066400000000000000000000007331476273367000265560ustar00rootroot00000000000000package ebandal.libreoffice.comp.tests.uno; import static org.junit.Assert.assertNotNull; import org.junit.Before; import org.junit.Test; import com.sun.star.text.XTextDocument; import ebandal.libreoffice.comp.tests.helper.UnoHelper; public class WriterTest { private XTextDocument xTextDocument; @Before public void setUp() throws Exception { xTextDocument = UnoHelper.getWriterDocument(); } @Test public void test() { assertNotNull(xTextDocument); } } H2Orestart-0.7.2/source/soffice/000077500000000000000000000000001476273367000164525ustar00rootroot00000000000000H2Orestart-0.7.2/source/soffice/ConvEquation.java000066400000000000000000000460271476273367000217410ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package soffice; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.sun.star.beans.XPropertySet; import com.sun.star.document.XEmbeddedObjectSupplier2; import com.sun.star.lang.XComponent; import com.sun.star.text.ControlCharacter; import com.sun.star.text.HoriOrientation; import com.sun.star.text.RelOrientation; import com.sun.star.text.TextContentAnchorType; import com.sun.star.text.VertOrientation; import com.sun.star.text.XText; import com.sun.star.text.XTextContent; import com.sun.star.text.XTextCursor; import com.sun.star.text.XTextFrame; import com.sun.star.uno.Exception; import com.sun.star.uno.UnoRuntime; import HwpDoc.paragraph.Ctrl_EqEdit; public class ConvEquation { private static final Logger log = Logger.getLogger(ConvEquation.class.getName()); private static int autoNum = 0; public static void reset(WriterContext wContext) { autoNum = 0; } public static void addFormula(WriterContext wContext, Ctrl_EqEdit eq, int step) { String formula = convertEquation(eq.eqn); boolean hasCaption = eq.caption==null?false:eq.caption.size()==0?false:true; XTextFrame xFrame = null; XText xFrameText = null; XTextCursor xFrameCursor = null; try { if (hasCaption) { xFrame = ConvGraphics.makeOuterFrame(wContext, eq, false, step); // Frame 내부 Cursor 생성 xFrameText = xFrame.getText(); xFrameCursor = xFrameText.createTextCursor(); } Object obj = wContext.mMSF.createInstance("com.sun.star.text.TextEmbeddedObject"); XTextContent embedContent = UnoRuntime.queryInterface(XTextContent.class, obj); if (embedContent == null) { log.severe("Could not create a formula embedded object"); return; } // set class ID for type of object being inserted XPropertySet props = UnoRuntime.queryInterface(XPropertySet.class, embedContent); props.setPropertyValue("CLSID", "078B7ABA-54FC-457F-8551-6147e776a997"); // a formula props.setPropertyValue("AnchorType", TextContentAnchorType.AS_CHARACTER); props.setPropertyValue("Height", Transform.translateHwp2Office(eq.height)); props.setPropertyValue("Width", Transform.translateHwp2Office(eq.width)); if (hasCaption) { props.setPropertyValue("VertOrient", VertOrientation.CENTER); // Top, Bottom, Center, fromBottom props.setPropertyValue("VertOrientRelation", RelOrientation.TEXT_LINE); // Base line, Character, Row props.setPropertyValue("HoriOrient", HoriOrientation.CENTER); // 0:NONE=From left props.setPropertyValue("HoriOrientRelation", RelOrientation.PRINT_AREA); // 1:paragraph text area } if (hasCaption) { xFrameText.insertTextContent(xFrameCursor, embedContent, false); xFrameText.insertControlCharacter(xFrameCursor, ControlCharacter.PARAGRAPH_BREAK, false); } else { wContext.mText.insertTextContent(wContext.mTextCursor, embedContent, false); } // access object's model XEmbeddedObjectSupplier2 embedObjSupplier = UnoRuntime.queryInterface(XEmbeddedObjectSupplier2.class, embedContent); XComponent embedObjModel = embedObjSupplier.getEmbeddedObject(); XPropertySet formulaProps = UnoRuntime.queryInterface(XPropertySet.class, embedObjModel); formulaProps.setPropertyValue("Formula", formula); // 캡션 쓰기 if (hasCaption) { ConvGraphics.addCaptionString(wContext, xFrameText, xFrameCursor, eq, step); } } catch (Exception e) { e.printStackTrace(); } catch (SkipDrawingException e) { e.printStackTrace(); } } public static String replacePattern(String targetStr, String regex, String replaceWith) { Pattern pattern = Pattern.compile(regex); Matcher m = pattern.matcher(targetStr); StringBuffer sb = new StringBuffer(); int startIndex = 0; int endIndex = -1; while(m.find()) { startIndex = m.start(); if (startIndex > 0) sb.append(targetStr.substring(endIndex+1, startIndex)); endIndex = m.end(); String para1 = targetStr.substring(startIndex, endIndex); para1 = para1.replaceAll(regex, replaceWith); sb.append(para1); } if (endIndex < targetStr.length()) sb.append(targetStr.substring(endIndex+1)); return sb.toString(); } public static String convertEquation(String hwpString) { String retString = hwpString; retString = retString.replaceAll("(?=") .replaceAll("(?>>", "") // .replaceAll("BULLET", "").replaceAll("DEG", "").replaceAll("AST", "").replaceAll("STAR", "").replaceAll("BIGCIRC", "") // .replaceAll("SQSUBSET", "").replaceAll("SQSUPSET", "").replaceAll("SQSUBSETEQ", "").replaceAll("SQCAP", "").replaceAll("SQCUP", "") // .replaceAll("DAGGER", "").replaceAll("DDAGGER", "").replaceAll("LNOT", "").replaceAll("PROPTO", "").replaceAll("XOR", "") // .replaceAll("THEREFORE", "").replaceAll("BECAUSE", "").replaceAll("IDENTICAL", "").replaceAll("DOTEQ", "").replaceAll("image", "").replaceAll("REIMAGE", "") // .replaceAll("udarrow", "").replaceAll("lrarrow", "").replaceAll("UDARROW", "").replaceAll("UPARROW", "").replaceAll("DOWNARROW", "") // .replaceAll("nwarrow", "").replaceAll("searrow", "").replaceAll("nearrow", "").replaceAll("CONG", "") // .replaceAll("swarrow", "").replaceAll("hookleft", "").replaceAll("hookright", "").replaceAll("mapsto", "") // .replaceAll("TRIANGLE", "").replaceAll("ANGLE", "").replaceAll("MSANGLE", "").replaceAll("prime", "") // .replaceAll("ASYMP", "").replaceAll("ISO", "").replaceAll("DIAMOND", "").replaceAll("DSUM", "") // .replaceAll("RTANGLE", "").replaceAll("VDASH", "").replaceAll("HLEFT", "").replaceAll("TOP", "").replaceAll("MODELS", "") // .replaceAll("LAPLACE", "").replaceAll("CENTIGRADE", "").replaceAll("FAHRENHEIT", "").replaceAll("LSLANT", "").replaceAll("RSLANT", "") // .replaceAll("att", "").replaceAll("thou", "").replaceAll("well", "").replaceAll("base", "").replaceAll("benzene", "") // 동일. 변환할 필요없음. // .replaceAll("<<", "").replaceAll(">>", "").replaceAll("notin", "").replaceAll("uparrow", "").replaceAll("downarrow", "") // .replaceAll("acute", "").replaceAll("bar", "").replaceAll("grave", "").replaceAll("vec", "").replaceAll("dot", "").replaceAll("ddot", "") ; if (retString.toLowerCase().contains("bigg")) { if (retString.matches(".*(BIGG|bigg)\\s*\\/.*")) { retString = replacePattern(retString, "(BIGG|bigg)\\s*/\\s*(.*)", "wideslash {$2}"); } else if (retString.matches(".*(BIGG|bigg)\\s*\\\\\\s*(.*)")) { retString = replacePattern(retString, "(BIGG|bigg)\\s*\\\\\\s*(.*)", "widebslash {$2}"); } } if (retString.toLowerCase().contains("over")) { if (retString.matches(".*([^\\s\\}]+)\\s*(OVER|over)\\s*([^\\{\\s]+).*")) { retString = replacePattern(retString, "([^\\s]+)\\s*(OVER|over)\\s*([^\\s]+)", "{$1 over $3} "); } } if (retString.toLowerCase().contains("matrix")) { String param2 = retString.replaceAll(".*(MATRIX|matrix)\\s*\\{((\\{.+\\}|.+)+)\\}.*", "$2"); String newParam2 = param2.replaceAll("#", "##").replaceAll("&", "#"); if (retString.toLowerCase().contains("bmatrix")) { retString = replacePattern(retString, "(BMATRIX|bmatrix)\\s*\\{((\\{.+\\}|.+)+)\\}", "left [ matrix{ "+newParam2+"} right ]"); } else if (retString.toLowerCase().contains("dmatrix")) { retString = replacePattern(retString, "(DMATRIX|dmatrix)\\s*\\{((\\{.+\\}|.+)+)\\}", "left lline matrix{ "+newParam2+"} right rline"); } else if (retString.toLowerCase().contains("pmatrix")) { retString = replacePattern(retString, "(PMATRIX|pmatrix)\\s*\\{((\\{.+\\}|.+)+)\\}", "left ( matrix{ "+newParam2+"} right )"); } else { retString = replacePattern(retString, "(MATRIX|matrix)\\s*\\{((\\{.+\\}|.+)+)\\}", "matrix{ "+newParam2+"} "); } } if (retString.toLowerCase().contains("hat")) { String param1 = retString.replaceAll("hat\\s*(\\{?.*\\}?)\\s*.*", "$1"); if (param1.length()>1) { retString = replacePattern(retString, "hat\\s*(\\{?.*\\}?)\\s*.*", "widehat {$1}"); } } if (retString.toLowerCase().contains("check")) { String param1 = retString.replaceAll("check\\s*(\\{?.*\\}?)\\s*.*", "$1"); if (param1.length()>1) { retString = replacePattern(retString, "check\\s*(\\{?.*\\}?)\\s*.*", "widecheck {$1}"); } } if (retString.toLowerCase().contains("tilde")) { String param1 = retString.replaceAll("tilde\\s*(\\{?.*\\}?)\\s*.*", "$1"); if (param1.length()>1) { retString = replacePattern(retString, "tilde\\s*(\\{?.*\\}?)\\s*.*", "widetilde {$1}"); } } if (retString.toLowerCase().contains(" atop ")) { retString = replacePattern(retString, "(\\{?\\s*.+\\s*\\}?)\\s+(ATOP|atop)\\s+(\\{?\\s*.+\\s*\\}?)", "binom $1 $3"); } if (retString.toLowerCase().contains("sum")) { if (retString.matches(".*(SUM|sum)\\s?\\_\\s*(\\{[^\\}]+\\}|[^\\s]+)\\s*\\^(\\{.+\\}|[^\\s]+)\\s*.*")) { retString = replacePattern(retString, "(SUM|sum)\\s?\\_\\s*(\\{[^\\}]+\\}|[^\\s]+)\\s*\\^(\\{.+\\}|[^\\s]+)", "sum from {$2} to {$3} "); } } if (retString.toLowerCase().contains("lim")) { if (retString.matches(".*(Lim|lim)\\s?\\_\\s*(\\{[^\\}]+\\}|[^\\s]+)\\s.*")) { retString = replacePattern(retString, "(Lim|lim)\\s?\\_\\s*(\\{[^\\}]+\\}|[^\\s]+)", "lim from {$2}"); } } if (retString.toLowerCase().contains("color")) { if (retString.matches(".*(COLOR|Color|color)\\s*\\{\\s*(\\d+)\\s*\\,\\s*\\d+\\s*\\,\\s*\\d+\\s*\\}.*")) { retString = replacePattern(retString, "(COLOR|color)\\s*\\{\\s*(\\d+)\\s*\\,\\s*(\\d+)\\s*\\,\\s*(\\d+)\\s*\\}", "color rgb $2 $3 $4 "); } } return retString; } } H2Orestart-0.7.2/source/soffice/ConvFootnote.java000066400000000000000000000174131476273367000217460ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package soffice; import java.util.Optional; import java.util.logging.Logger; import com.sun.star.container.XIndexAccess; import com.sun.star.text.XFootnote; import com.sun.star.text.XFootnotesSupplier; import com.sun.star.text.XText; import com.sun.star.text.XTextContent; import com.sun.star.text.XTextCursor; import com.sun.star.uno.Exception; import com.sun.star.uno.UnoRuntime; import HwpDoc.HwpElement.HwpRecord_CharShape; import HwpDoc.HwpElement.HwpRecord_ParaShape; import HwpDoc.HwpElement.HwpRecord_Style; import HwpDoc.paragraph.Ctrl; import HwpDoc.paragraph.Ctrl_AutoNumber; import HwpDoc.paragraph.Ctrl_Character; import HwpDoc.paragraph.Ctrl_Note; import HwpDoc.paragraph.HwpParagraph; import HwpDoc.paragraph.ParaText; public class ConvFootnote { private static final Logger log = Logger.getLogger(ConvFootnote.class.getName()); private static int footnoteIndex = 0; public static int getFootnoteIndex() { return footnoteIndex; } public static void setFootnoteIndex(int index) { footnoteIndex = index; } public static void reset(WriterContext wContext) { footnoteIndex = 0; } protected static void insertFootnote(WriterContext wContext, Ctrl_Note note, int step) { try { XFootnote xFootnote = UnoRuntime.queryInterface(XFootnote.class, wContext.mMSF.createInstance("com.sun.star.text.Footnote")); XTextContent xContent = UnoRuntime.queryInterface(XTextContent.class, xFootnote); wContext.mText.insertTextContent(wContext.mTextCursor, xContent, false); XFootnotesSupplier xFootnoteSupplier = UnoRuntime.queryInterface(XFootnotesSupplier.class, wContext.mMyDocument); XIndexAccess xFootnotes = UnoRuntime.queryInterface(XIndexAccess.class, xFootnoteSupplier.getFootnotes()); XFootnote xNumbers = UnoRuntime.queryInterface(XFootnote.class, xFootnotes.getByIndex(getFootnoteIndex())); XText xSimple = UnoRuntime.queryInterface(XText.class, xNumbers); XTextCursor xRange = UnoRuntime.queryInterface(XTextCursor.class, xSimple.createTextCursor()); WriterContext context2 = new WriterContext(); context2.mContext = wContext.mContext; context2.mDesktop = wContext.mDesktop; context2.mMCF = wContext.mMCF; context2.mMSF = wContext.mMSF; context2.mMyDocument = wContext.mMyDocument; context2.userHomeDir = wContext.userHomeDir; context2.mText = xSimple; context2.mTextCursor = xRange; if (note.paras != null) { for (int paraIndex = 0; paraIndex < note.paras.size(); paraIndex++) { HwpParagraph para = note.paras.get(paraIndex); if (para.p == null || para.p.size() == 0) continue; boolean isLastPara = (paraIndex == note.paras.size() - 1) ? true : false; String styleName = ConvPara.getStyleName((int) para.paraStyleID); log.finer("StyleID=" + para.paraStyleID + ", StyleName=" + styleName); if (styleName == null || styleName.isEmpty()) { log.fine("Style Name is empty"); } short[] charShapeID = new short[1]; Optional ctrlOp = para.p.stream().findFirst(); if (ctrlOp.isPresent()) { if (ctrlOp.get() instanceof ParaText) { charShapeID[0] = (short) ((ParaText) ctrlOp.get()).charShapeId; } else if (ctrlOp.get() instanceof Ctrl_Character) { charShapeID[0] = (short) ((Ctrl_Character) ctrlOp.get()).charShapeId; } } HwpRecord_Style paraStyle = wContext.getParaStyle(para.paraStyleID); HwpRecord_ParaShape paraShape = wContext.getParaShape(para.paraShapeID); HwpCallback callback = new HwpCallback() { @Override public void onNewNumber(int paraStyleID, int paraShapeID) { reset(context2); String label = Integer.valueOf(getFootnoteIndex() + 1).toString() + ")"; // index 보다 1많은 값으로 // 표현. xFootnote.setLabel(label); }; @Override public void onAutoNumber(Ctrl_AutoNumber autoNumber, int paraStyleID, int paraShapeID) { String label = Integer.valueOf(getFootnoteIndex() + 1).toString() + ")"; // index 보다 1많은 값으로 // 표현. xFootnote.setLabel(label); }; @Override public boolean onTab(String info) { HwpRecord_CharShape charShape = wContext.getCharShape(charShapeID[0]); HwpRecurs.insertParaString(context2, "\t", para.lineSegs, styleName, paraStyle, paraShape, charShape, true, true, step); return true; }; @Override public boolean onText(String content, int charShapeId, int charPos, boolean append) { charShapeID[0] = (short) charShapeId; HwpRecord_CharShape charShape = wContext.getCharShape(charShapeID[0]); HwpRecurs.insertParaString(context2, content, para.lineSegs, styleName, paraStyle, paraShape, charShape, append, true, step); // xSimple.insertString (xRange, content, false ); return true; } @Override public boolean onParaBreak() { if (isLastPara == false) { HwpRecord_CharShape charShape = wContext.getCharShape(charShapeID[0]); HwpRecurs.insertParaString(context2, "\r", para.lineSegs, styleName, paraStyle, paraShape, charShape, true, true, step); } return true; } }; HwpRecurs.printParaRecurs(context2, wContext, para, callback, 2); } } } catch (Exception e) { e.printStackTrace(); } setFootnoteIndex(getFootnoteIndex() + 1); } } H2Orestart-0.7.2/source/soffice/ConvGraphics.java000066400000000000000000005155621476273367000217210ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package soffice; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.IntStream; import javax.imageio.ImageIO; import java.util.NoSuchElementException; import java.util.Optional; import com.sun.star.awt.Point; import com.sun.star.awt.Size; import com.sun.star.beans.PropertyValue; import com.sun.star.beans.PropertyVetoException; import com.sun.star.beans.UnknownPropertyException; import com.sun.star.beans.XPropertySet; import com.sun.star.container.XNameContainer; import com.sun.star.container.XNameAccess; import com.sun.star.drawing.BitmapMode; import com.sun.star.drawing.CircleKind; import com.sun.star.drawing.FillStyle; import com.sun.star.drawing.HomogenMatrix3; import com.sun.star.drawing.LineEndType; import com.sun.star.drawing.PolyPolygonBezierCoords; import com.sun.star.drawing.PolygonFlags; import com.sun.star.drawing.TextHorizontalAdjust; import com.sun.star.drawing.TextVerticalAdjust; import com.sun.star.drawing.XShape; import com.sun.star.graphic.XGraphic; import com.sun.star.graphic.XGraphicProvider; import com.sun.star.lang.IllegalArgumentException; import com.sun.star.lang.WrappedTargetException; import com.sun.star.lib.uno.adapter.ByteArrayToXInputStreamAdapter; import com.sun.star.style.ParagraphAdjust; import com.sun.star.table.BorderLine2; import com.sun.star.table.BorderLineStyle; import com.sun.star.text.ControlCharacter; import com.sun.star.text.HoriOrientation; import com.sun.star.text.RelOrientation; import com.sun.star.text.SizeType; import com.sun.star.text.TextContentAnchorType; import com.sun.star.text.VertOrientation; import com.sun.star.text.WrapTextMode; import com.sun.star.text.WritingMode2; import com.sun.star.text.XParagraphCursor; import com.sun.star.text.XText; import com.sun.star.text.XTextContent; import com.sun.star.text.XTextCursor; import com.sun.star.text.XTextFrame; import com.sun.star.uno.Exception; import com.sun.star.uno.UnoRuntime; import HwpDoc.HwpElement.HwpRecordTypes.LineArrowSize; import HwpDoc.HwpElement.HwpRecordTypes.LineArrowStyle; import HwpDoc.HwpElement.HwpRecord_BorderFill; import HwpDoc.HwpElement.HwpRecord_BorderFill.Fill; import HwpDoc.HwpElement.HwpRecord_BorderFill.ImageFillType; import HwpDoc.HwpElement.HwpRecord_CharShape; import HwpDoc.HwpElement.HwpRecord_ParaShape; import HwpDoc.paragraph.Ctrl; import HwpDoc.paragraph.Ctrl_AutoNumber; import HwpDoc.paragraph.Ctrl_Character; import HwpDoc.paragraph.Ctrl_Common; import HwpDoc.paragraph.Ctrl_Container; import HwpDoc.paragraph.Ctrl_GeneralShape; import HwpDoc.paragraph.Ctrl_ShapeArc; import HwpDoc.paragraph.Ctrl_ShapeCurve; import HwpDoc.paragraph.Ctrl_ShapeEllipse; import HwpDoc.paragraph.Ctrl_ShapeLine; import HwpDoc.paragraph.Ctrl_ShapePic; import HwpDoc.paragraph.Ctrl_ShapePolygon; import HwpDoc.paragraph.Ctrl_ShapeRect; import HwpDoc.paragraph.Ctrl_ShapeVideo; import HwpDoc.paragraph.Ctrl_Table; import HwpDoc.paragraph.HwpParagraph; import HwpDoc.paragraph.ParaText; import HwpDoc.section.Page; import soffice.HwpCallback.TableFrame; public class ConvGraphics { private static final Logger log = Logger.getLogger(ConvGraphics.class.getName()); private static int autoNum = 0; public static void reset(WriterContext wContext) { autoNum = 0; } public static void insertGraphic(WriterContext wContext, Ctrl_GeneralShape obj, short paraShapeID, int step) { HwpRecord_ParaShape paraShape = wContext.getParaShape((short) paraShapeID); XParagraphCursor paraCursor = UnoRuntime.queryInterface(XParagraphCursor.class, wContext.mTextCursor); XPropertySet paraProps = UnoRuntime.queryInterface(XPropertySet.class, paraCursor); ConvPara.setParagraphProperties(paraProps, paraShape, wContext.getDocInfo().compatibleDoc, ConvPara.PARA_SPACING); switch (obj.ctrlId) { case "cip$": insertPICTURE(wContext, (Ctrl_ShapePic) obj, step, -1, -1); break; case "div$": insertVIDEO(wContext, (Ctrl_ShapeVideo) obj, step, -1, -1); break; case "cer$": if (obj.paras == null || obj.paras.size() < 1) { insertRECTANGLE(wContext, (Ctrl_ShapeRect) obj, step, -1, -1); } else { insertTextFrame(wContext, (Ctrl_ShapeRect) obj, step, -1, -1); } break; case "nil$": // 선 case "loc$": insertLINE(wContext, (Ctrl_ShapeLine) obj, step, -1, -1); break; case "lle$": // 타원 insertELLIPSE(wContext, (Ctrl_ShapeEllipse) obj, step, -1, -1); break; case "lop$": // 다각형 insertPOLYGON(wContext, (Ctrl_ShapePolygon) obj, step, -1, -1); break; case "ruc$": // 곡선 insertCURVE(wContext, (Ctrl_ShapeCurve) obj, step); break; case "cra$": insertARC(wContext, (Ctrl_ShapeArc) obj, step, -1, -1); break; case "noc$": // 묶음 개체 insertMulti(wContext, (Ctrl_Container) obj, step); break; case "elo$": // OLE case "tat$": // 글맵시 insertDummyTextFrame(wContext, (Ctrl_GeneralShape) obj, step); break; default: break; } } private static void insertPICTURE(WriterContext wContext, Ctrl_ShapePic pic, int step, int shapeWidth, int shapeHeight) { boolean hasCaption = pic.caption == null ? false : pic.caption.size() == 0 ? false : true; XTextFrame xFrame = null; XText xFrameText = null; XTextCursor xFrameCursor = null; try { if (hasCaption) { xFrame = makeOuterFrame(wContext, pic, false, step); // Frame 내부 Cursor 생성 xFrameText = xFrame.getText(); xFrameCursor = xFrameText.createTextCursor(); } int sizeWidth = 0, sizeHeight = 0; if (shapeWidth <= 0 && shapeHeight <= 0) { /* transform을 거치지 않는 TextGrapicObject는 curWidth curHeight 로 크기 설정 */ sizeWidth = Math.abs(pic.curWidth); sizeHeight = Math.abs(pic.curHeight); if (sizeWidth==0 || sizeHeight==0) { sizeWidth = Math.abs(pic.width); sizeHeight = Math.abs(pic.height); } } else { sizeWidth = shapeWidth; sizeHeight = shapeHeight; } // 그림그리기 Object textGraphicObject = wContext.mMSF.createInstance("com.sun.star.text.TextGraphicObject"); XTextContent xTextContent = (XTextContent) UnoRuntime.queryInterface(XTextContent.class, textGraphicObject); XPropertySet xPropSet = (XPropertySet) UnoRuntime.queryInterface(XPropertySet.class, textGraphicObject); // image ByteArray로 그림 그리기 Object graphicProviderObject = wContext.mMCF .createInstanceWithContext("com.sun.star.graphic.GraphicProvider", wContext.mContext); XGraphicProvider xGraphicProvider = UnoRuntime.queryInterface(XGraphicProvider.class, graphicProviderObject); byte[] imageAsByteArray = null; String imageType = ""; imageAsByteArray = wContext.getBinBytes(pic.binDataID); imageType = wContext.getBinFormat(pic.binDataID); if (imageAsByteArray == null || imageAsByteArray.length == 0) { log.severe("Something Wrong!!!. skip drawing"); return; } PropertyValue[] v = new PropertyValue[2]; v[0] = new PropertyValue(); v[0].Name = "InputStream"; v[0].Value = new ByteArrayToXInputStreamAdapter(imageAsByteArray); v[1] = new PropertyValue(); v[1].Name = "MimeType"; switch (imageType.toLowerCase()) { case "png": v[1].Value = "image/png"; break; case "bmp": v[1].Value = "image/bmp"; break; case "wmf": v[1].Value = "image/x-wmf"; break; case "jpg": v[1].Value = "image/jpeg"; break; case "gif": v[1].Value = "image/gif"; break; case "tif": v[1].Value = "image/tiff"; break; case "svg": v[1].Value = "image/svg+xml"; break; } XGraphic graphic = xGraphicProvider.queryGraphic(v); if (graphic == null) { log.severe("Error loading the image"); } else { if (pic.cropLeft>0 || pic.cropRight>0 || pic.cropTop>0 || pic.cropBottom>0) { /* 이미지 원본이 페이지보다 크면 원본이미지가 아닌 페이지크기에서 crop하므로 원하는 그림을 가져오지 못한다. GraphicCrop crop = new GraphicCrop(); crop.Left = Transform.translateHwp2Office(pic.cropLeft); crop.Right = Transform.translateHwp2Office(pic.iniPicWidth-pic.cropRight); crop.Top = Transform.translateHwp2Office(pic.cropTop); crop.Bottom = Transform.translateHwp2Office(pic.iniPicHeight-pic.cropBottom); xPropSet.setPropertyValue("GraphicCrop", crop); */ try { PropertyValue[] pv = new PropertyValue[2]; Path homeDir = wContext.userHomeDir; Path path = Files.createTempFile(homeDir, "H2O_IMG_", "_" + pic.binDataID + ".png"); URL url = path.toFile().toURI().toURL(); String urlString = url.toExternalForm(); pv[0] = new PropertyValue(); pv[0].Name = "URL"; pv[0].Value = urlString; pv[1] = new PropertyValue(); pv[1].Name = "MimeType"; pv[1].Value = "image/png"; xGraphicProvider.storeGraphic(graphic, pv); BufferedImage originalImage = ImageIO.read(path.toFile()); Files.delete(path); int orgWidth = pic.iniPicWidth==0 ? pic.iniWidth : pic.iniPicWidth; int imgWidth = originalImage.getWidth(); int imgHeight = originalImage.getHeight(); float hwp2pixelRatio = (float)imgWidth / orgWidth; int cropLeftPixel = (int)(pic.cropLeft*hwp2pixelRatio); int cropTopPixel = (int)(pic.cropTop*hwp2pixelRatio); int cropWidthPixel = (int)((pic.cropRight-pic.cropLeft)*hwp2pixelRatio); int cropHeightPixel = (int)((pic.cropBottom-pic.cropTop)*hwp2pixelRatio); int subLeft = cropLeftPixel>imgWidth ? 0 : cropLeftPixel; int subTop = cropTopPixel>imgHeight ? 0 : cropTopPixel; int subWidth = Math.min(cropWidthPixel, imgWidth-subLeft); int subHeight = Math.min(cropHeightPixel, imgHeight-subTop); BufferedImage subImgage = originalImage.getSubimage(subLeft, subTop, subWidth, subHeight); try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { ImageIO.write(subImgage, "png", baos); imageAsByteArray = baos.toByteArray(); imageType = "png"; pv[0] = new PropertyValue(); pv[0].Name = "InputStream"; pv[0].Value = new ByteArrayToXInputStreamAdapter(imageAsByteArray); pv[1] = new PropertyValue(); pv[1].Name = "MimeType"; pv[1].Value = "image/png"; graphic = xGraphicProvider.queryGraphic(pv); } } catch (IOException e) { e.printStackTrace(); } } xPropSet.setPropertyValue("Graphic", graphic); } if (hasCaption) { try { xPropSet.setPropertyValue("AnchorType", TextContentAnchorType.AS_CHARACTER); } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("AnchorType has Exception"); } xPropSet.setPropertyValue("VertOrient", VertOrientation.CENTER); // Top, Bottom, Center, fromBottom xPropSet.setPropertyValue("VertOrientRelation", RelOrientation.TEXT_LINE); // Base line, Character, Row xPropSet.setPropertyValue("HoriOrient", HoriOrientation.CENTER); // 0:NONE=From left xPropSet.setPropertyValue("HoriOrientRelation", RelOrientation.PRINT_AREA); // 1:paragraph text area } else { double xScale = pic.matrixSeq == null ? 1.0 : pic.matrixSeq[0]; double yScale = pic.matrixSeq == null ? 1.0 : pic.matrixSeq[4]; setPosition(xPropSet, pic, (int) (pic.nGrp>0 ? (pic.vertRelTo==null?0:pic.xGrpOffset*xScale) : 0), (int) (pic.nGrp>0 ? (pic.horzRelTo==null?0:pic.yGrpOffset*yScale) : 0)); } setWrapStyle(xPropSet, pic); setLineStyle(xPropSet, pic); // 위치를 잡은 후에 크기를 조정한다. xPropSet.setPropertyValue("Width", Transform.translateHwp2Office(sizeWidth)); xPropSet.setPropertyValue("Height", Transform.translateHwp2Office(sizeHeight)); // setLineStyle 대신 border 속성값을 직접 넣는다. if (pic instanceof Ctrl_ShapePic) { BorderLine2 pictureBorder = Transform.toBorderLine2(pic); xPropSet.setPropertyValue("TopBorder", pictureBorder); xPropSet.setPropertyValue("BottomBorder", pictureBorder); xPropSet.setPropertyValue("LeftBorder", pictureBorder); xPropSet.setPropertyValue("RightBorder", pictureBorder); } if (hasCaption) { xFrameText.insertTextContent(xFrameCursor, xTextContent, true); xFrameText.insertControlCharacter(xFrameCursor, ControlCharacter.PARAGRAPH_BREAK, false); } else { wContext.mText.insertTextContent(wContext.mTextCursor, xTextContent, true); if (wContext.version >= 72) { TextContentAnchorType anchorType = (TextContentAnchorType) xPropSet.getPropertyValue("AnchorType"); if (anchorType == TextContentAnchorType.AT_PARAGRAPH) { wContext.mText.insertString(wContext.mTextCursor, " ", false); } } } if (pic.nGrp == 0) { ++autoNum; } // 캡션 쓰기 if (hasCaption) { addCaptionString(wContext, xFrameText, xFrameCursor, pic, step); } } catch (com.sun.star.uno.Exception e) { e.printStackTrace(); } catch (SkipDrawingException e) { e.printStackTrace(); } } private static void insertVIDEO(WriterContext wContext, Ctrl_ShapeVideo vid, int step, int shapeWidth, int shapeHeight) { boolean hasCaption = vid.caption == null ? false : vid.caption.size() == 0 ? false : true; XTextFrame xFrame = null; XText xFrameText = null; XTextCursor xFrameCursor = null; try { if (hasCaption) { xFrame = makeOuterFrame(wContext, vid, false, step); // Frame 내부 Cursor 생성 xFrameText = xFrame.getText(); xFrameCursor = xFrameText.createTextCursor(); } int sizeWidth = 0, sizeHeight = 0; if (shapeWidth <= 0 && shapeHeight <= 0) { /* transform을 거치지 않는 TextGrapicObject는 curWidth curHeight 로 크기 설정 */ sizeWidth = Math.abs(vid.width); sizeHeight = Math.abs(vid.height); if (sizeWidth==0 || sizeHeight==0) { sizeWidth = Math.abs(vid.width); sizeHeight = Math.abs(vid.height); } } else { sizeWidth = shapeWidth; sizeHeight = shapeHeight; } // 그림그리기 Object textGraphicObject = wContext.mMSF.createInstance("com.sun.star.text.TextGraphicObject"); XTextContent xTextContent = (XTextContent) UnoRuntime.queryInterface(XTextContent.class, textGraphicObject); XPropertySet xPropSet = (XPropertySet) UnoRuntime.queryInterface(XPropertySet.class, textGraphicObject); // image ByteArray로 그림 그리기 Object graphicProviderObject = wContext.mMCF .createInstanceWithContext("com.sun.star.graphic.GraphicProvider", wContext.mContext); XGraphicProvider xGraphicProvider = UnoRuntime.queryInterface(XGraphicProvider.class, graphicProviderObject); byte[] imageAsByteArray = wContext.getBinBytes(vid.thumnailBinID); if (imageAsByteArray == null || imageAsByteArray.length == 0) { log.severe("Something Wrong!!!. skip drawing"); return; } PropertyValue[] v = new PropertyValue[2]; v[0] = new PropertyValue(); v[0].Name = "InputStream"; v[0].Value = new ByteArrayToXInputStreamAdapter(imageAsByteArray); v[1] = new PropertyValue(); v[1].Name = "MimeType"; switch (wContext.getBinFormat(vid.thumnailBinID).toLowerCase()) { case "png": v[1].Value = "image/png"; break; case "bmp": v[1].Value = "image/bmp"; break; case "wmf": v[1].Value = "image/x-wmf"; break; case "jpg": v[1].Value = "image/jpeg"; break; case "gif": v[1].Value = "image/gif"; break; case "tif": v[1].Value = "image/tiff"; break; case "svg": v[1].Value = "image/svg+xml"; break; } XGraphic graphic = xGraphicProvider.queryGraphic(v); if (graphic == null) { log.severe("Error loading the image"); } else { xPropSet.setPropertyValue("Graphic", graphic); } // image ByteArray로 그림 그리기 if (hasCaption) { try { xPropSet.setPropertyValue("AnchorType", TextContentAnchorType.AS_CHARACTER); } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("AnchorType has Exception"); } xPropSet.setPropertyValue("VertOrient", VertOrientation.CENTER); // Top, Bottom, Center, fromBottom xPropSet.setPropertyValue("VertOrientRelation", RelOrientation.TEXT_LINE); // Base line, Character, Row xPropSet.setPropertyValue("HoriOrient", HoriOrientation.CENTER); // 0:NONE=From left xPropSet.setPropertyValue("HoriOrientRelation", RelOrientation.PRINT_AREA); // 1:paragraph text area } else { double xScale = vid.matrixSeq == null ? 1.0 : vid.matrixSeq[0]; double yScale = vid.matrixSeq == null ? 1.0 : vid.matrixSeq[4]; setPosition(xPropSet, vid, (int) (vid.nGrp > 0 ? vid.xGrpOffset * xScale : 0), (int) (vid.nGrp > 0 ? vid.yGrpOffset * yScale : 0)); } setWrapStyle(xPropSet, vid); // 위치를 잡은 후에 크기를 조정한다. xPropSet.setPropertyValue("Width", Transform.translateHwp2Office(sizeWidth)); xPropSet.setPropertyValue("Height", Transform.translateHwp2Office(sizeHeight)); if (hasCaption) { xFrameText.insertTextContent(xFrameCursor, xTextContent, true); xFrameText.insertControlCharacter(xFrameCursor, ControlCharacter.PARAGRAPH_BREAK, false); } else { wContext.mText.insertTextContent(wContext.mTextCursor, xTextContent, true); if (wContext.version >= 72) { TextContentAnchorType anchorType = (TextContentAnchorType) xPropSet.getPropertyValue("AnchorType"); if (anchorType == TextContentAnchorType.AT_PARAGRAPH) { wContext.mText.insertString(wContext.mTextCursor, " ", false); } } } if (vid.nGrp == 0) { ++autoNum; } // 캡션 쓰기 if (hasCaption) { addCaptionString(wContext, xFrameText, xFrameCursor, vid, step); } } catch (com.sun.star.uno.Exception e) { e.printStackTrace(); } catch (SkipDrawingException e) { e.printStackTrace(); } } private static void insertMulti(WriterContext wContext, Ctrl_Container container, int step) { boolean hasCaption = container.caption == null ? false : container.caption.size() == 0 ? false : true; XTextFrame xFrame = null; XText xFrameText = null; XTextCursor xFrameCursor = null; XPropertySet paraProps = null; try { xFrame = makeOuterFrame(wContext, container, false, step); // Frame 내부 Cursor 생성 xFrameText = xFrame.getText(); xFrameCursor = xFrameText.createTextCursor(); WriterContext frameContext = new WriterContext(); frameContext.mContext = wContext.mContext; frameContext.mDesktop = wContext.mDesktop; frameContext.mMCF = wContext.mMCF; frameContext.mMSF = wContext.mMSF; frameContext.mMyDocument = wContext.mMyDocument; frameContext.userHomeDir = wContext.userHomeDir; frameContext.mText = xFrameText; frameContext.mTextCursor = xFrameCursor; for (Ctrl_GeneralShape shape : container.list) { double xScale = 1.0, yScale = 1.0; double radian = 0.0; int sizeWidth = 0, sizeHeight = 0; for (int i = 0; i < shape.matCnt; i++) { xScale *= shape.matrixSeq[i * 12 + 0]; yScale *= shape.matrixSeq[i * 12 + 4]; radian += Math.atan2(shape.matrixSeq[i * 12 + 9], shape.matrixSeq[i * 12 + 6]); } if (radian != 0.0 && shape.rotat == 0) { shape.rotat = (short) Math.toDegrees(radian); } switch (shape.getClass().getSimpleName()) { case "Ctrl_ShapeArc": case "Ctrl_ShapeEllipse": case "Ctrl_ShapeRect": case "Ctrl_ShapePolygon": case "Ctrl_ShapePic": sizeWidth = shape.curWidth; sizeHeight = shape.curHeight; // 2레벨 container(nGrp>=2) 에서는 무조건 scale 연산을 하도록. if (shape.nGrp >= 1 || (shape.curWidth != shape.iniWidth || shape.curHeight != shape.iniHeight)) { sizeWidth = (int) (shape.iniWidth * xScale /* /container.matrixSeq[0] */); sizeHeight = (int) (shape.iniHeight * yScale /* /container.matrixSeq[4] */); } break; case "Ctrl_ShapeLine": sizeWidth = (((Ctrl_ShapeLine) shape).endX - ((Ctrl_ShapeLine) shape).startX); sizeHeight = (((Ctrl_ShapeLine) shape).endY - ((Ctrl_ShapeLine) shape).startY); sizeWidth = (int) (shape.iniWidth * xScale /* /container.matrixSeq[0] */); sizeHeight = (int) (shape.iniHeight * yScale /* /container.matrixSeq[4] */); break; case "Ctrl_Container": default: } if (shape instanceof Ctrl_ShapeArc) { Ctrl_ShapeArc arc = (Ctrl_ShapeArc) shape; insertARC(frameContext, arc, step + 1, sizeWidth, sizeHeight); } else if (shape instanceof Ctrl_ShapeEllipse) { Ctrl_ShapeEllipse ell = (Ctrl_ShapeEllipse) shape; insertELLIPSE(frameContext, ell, step + 1, sizeWidth, sizeHeight); } else if (shape instanceof Ctrl_ShapeRect) { Ctrl_ShapeRect rect = (Ctrl_ShapeRect) shape; if (rect.paras == null || rect.paras.size() < 1) { insertRECTANGLE(frameContext, rect, step + 1, sizeWidth, sizeHeight); } else { insertTextFrame(frameContext, rect, step + 1, sizeWidth, sizeHeight); } } else if (shape instanceof Ctrl_ShapePolygon) { // Polygon 내부에 테이블이 있는 경우 LibreOffice에서는 틀(Frame)으로 변환한다. LibreOffice에서 테이블을 넣을수 // 있는 개체는 Frame뿐인듯 하다. boolean hasTable = shape.paras == null ? false : shape.paras.stream().anyMatch(para -> { if (para.p == null || para.p.size() == 0) return false; return para.p.stream().anyMatch(ctrl -> ctrl instanceof Ctrl_Table); }); if (hasTable) { insertTextFrame(frameContext, shape, step + 1, sizeWidth, sizeHeight); } else { Ctrl_ShapePolygon pol = (Ctrl_ShapePolygon) shape; insertPOLYGON(frameContext, pol, step + 1, sizeWidth, sizeHeight); } } else if (shape instanceof Ctrl_ShapePic) { Ctrl_ShapePic pic = (Ctrl_ShapePic) shape; String imageFormat = wContext.getBinFormat(pic.binDataID).toLowerCase(); // PICTURE는 translate이 되지 않으므로 묶음개체일때 이미지 배경이 있는 사각형으로 처리 if ("bmp".equals(imageFormat)) { // bmp포맷은 storeGraphic으로 저장되지 않는다. 파악될때까지 insertPICTURE()로 처리 insertPICTURE(frameContext, pic, step + 1, sizeWidth, sizeHeight); } else { insertPictureRECTAGLE(frameContext, pic, step + 1, sizeWidth, sizeHeight); } } else if (shape instanceof Ctrl_ShapeLine) { Ctrl_ShapeLine lin = (Ctrl_ShapeLine) shape; insertLINE(frameContext, lin, step + 1, sizeWidth, sizeHeight); } else if (shape instanceof Ctrl_Container) { Ctrl_Container con = (Ctrl_Container) shape; insertMulti(frameContext, con, step + 1); } } if (container.nGrp == 0) { ++autoNum; } // 캡션 쓰기 if (hasCaption) { xFrameText.insertControlCharacter(xFrameCursor, ControlCharacter.PARAGRAPH_BREAK, false); addCaptionString(wContext, xFrameText, xFrameCursor, container, step); } } catch (com.sun.star.uno.Exception e) { e.printStackTrace(); } catch (SkipDrawingException e) { e.printStackTrace(); } } private static void insertDummyTextFrame(WriterContext wContext, Ctrl_GeneralShape shape, int step) { try { Object oFrame = wContext.mMSF.createInstance("com.sun.star.text.TextFrame"); XTextFrame xFrame = (XTextFrame) UnoRuntime.queryInterface(XTextFrame.class, oFrame); if (xFrame == null) { log.severe("Could not create a text frame"); return; } XShape tfShape = UnoRuntime.queryInterface(XShape.class, xFrame); tfShape.setSize( new Size(Transform.translateHwp2Office(shape.width), Transform.translateHwp2Office(shape.height))); XPropertySet frameProps = UnoRuntime.queryInterface(XPropertySet.class, xFrame); setPosition(frameProps, shape, 0, 0); setWrapStyle(frameProps, shape); // dummy에서는 점선, 가는 회색 테두리로 그리고, 내부에는 "Not Supported Object" 회색 글씨를 넣도록 한다. BorderLine2 border = new BorderLine2(); border.Color = 0x808080; // GREY border.LineStyle = BorderLineStyle.DOTTED; border.InnerLineWidth = 0; border.OuterLineWidth = 0; border.LineDistance = 0; border.LineWidth = 35; // 10=0.3pt, 35=1pt frameProps.setPropertyValue("TopBorder", border); frameProps.setPropertyValue("BottomBorder", border); frameProps.setPropertyValue("LeftBorder", border); frameProps.setPropertyValue("RightBorder", border); XText xText = wContext.mTextCursor.getText(); xText.insertTextContent(wContext.mTextCursor, xFrame, false); if (wContext.version >= 72) { TextContentAnchorType anchorType = (TextContentAnchorType) frameProps.getPropertyValue("AnchorType"); if (anchorType == TextContentAnchorType.AT_PARAGRAPH) { xText.insertString(wContext.mTextCursor, " ", false); } } frameProps.setPropertyValue("FrameIsAutomaticHeight", false); // TextFrame을 그린 후에 automaticHeight를 조정해야.. frameProps.setPropertyValue("TextVerticalAdjust", TextVerticalAdjust.CENTER); wContext.mTextCursor.gotoEnd(false); XText xFrameText = xFrame.getText(); XTextCursor xFrameCursor = xFrameText.createTextCursor(); XParagraphCursor paraCursor = UnoRuntime.queryInterface(XParagraphCursor.class, xFrameCursor); XPropertySet paraProps = UnoRuntime.queryInterface(XPropertySet.class, paraCursor); paraProps.setPropertyValue("CharColor", 0x808080); // 회색글씨 paraProps.setPropertyValue("ParaAdjust", ParagraphAdjust.CENTER); xFrameText.insertString(xFrameCursor, "Not Supported Object", false); if (shape.nGrp == 0) { ++autoNum; } } catch (com.sun.star.uno.Exception e) { e.printStackTrace(); } catch (SkipDrawingException e) { e.printStackTrace(); } } private static void insertTextFrame(WriterContext wOuterContext, Ctrl_GeneralShape shape, int step, int shapeWidth, int shapeHeight) { try { Object oFrame = wOuterContext.mMSF.createInstance("com.sun.star.text.TextFrame"); XTextFrame xInternalFrame = (XTextFrame) UnoRuntime.queryInterface(XTextFrame.class, oFrame); if (xInternalFrame == null) { log.severe("Could not create a text frame"); return; } int sizeWidth = 0, sizeHeight = 0; if (shapeWidth <= 0 && shapeHeight <= 0) { sizeWidth = Math.abs(shape.curWidth); sizeHeight = Math.abs(shape.curHeight); if (sizeWidth==0 || sizeHeight==0) { sizeWidth = Math.abs(shape.width); sizeHeight = Math.abs(shape.height); } if (shape.rotat != 0) { Point2D ptSrc = new Point2D.Double(sizeWidth, sizeHeight); Point2D ptDst = Transform.rotateValue(shape.rotat, ptSrc); sizeWidth = (int) ptDst.getX(); sizeHeight = (int) ptDst.getY(); } } else { sizeWidth = shapeWidth; sizeHeight = shapeHeight; } XShape tfShape = UnoRuntime.queryInterface(XShape.class, xInternalFrame); tfShape.setSize( new Size(Transform.translateHwp2Office(sizeWidth), Transform.translateHwp2Office(sizeHeight))); // anchor the text frame XPropertySet frameProps = UnoRuntime.queryInterface(XPropertySet.class, xInternalFrame); frameProps.setPropertyValue("FrameIsAutomaticHeight", false); double xScale = shape.matrixSeq == null ? 1.0 : shape.matrixSeq[0]; double yScale = shape.matrixSeq == null ? 1.0 : shape.matrixSeq[4]; if (shape.nGrp > 0) { HomogenMatrix3 aHomogenMatrix3 = getTransformedMatrix(shape); setPositionLO(frameProps, shape, (int)aHomogenMatrix3.Line1.Column3, (int)aHomogenMatrix3.Line2.Column3); } else { setPosition(frameProps, shape, 0, 0); } setWrapStyle(frameProps, shape); // frameProps.setPropertyValue("ZOrder", shape.zOrder); frameProps.setPropertyValue("LeftMargin", 0); frameProps.setPropertyValue("RightMargin", 0); frameProps.setPropertyValue("TopMargin", 0); frameProps.setPropertyValue("BottomMargin", 0); BorderLine2 border = Transform.toBorderLine2(shape); frameProps.setPropertyValue("TopBorder", border); frameProps.setPropertyValue("BottomBorder", border); frameProps.setPropertyValue("LeftBorder", border); frameProps.setPropertyValue("RightBorder", border); frameProps.setPropertyValue("LeftBorderDistance", Transform.translateHwp2Office(shape.leftSpace) <= 100 ? 0 : Transform.translateHwp2Office(shape.leftSpace) - 100); frameProps.setPropertyValue("RightBorderDistance", Transform.translateHwp2Office(shape.rightSpace) <= 100 ? 0 : Transform.translateHwp2Office(shape.rightSpace) - 100); frameProps.setPropertyValue("TopBorderDistance", Transform.translateHwp2Office(shape.upSpace) <= 100 ? 0 : Transform.translateHwp2Office(shape.upSpace) - 100); frameProps.setPropertyValue("BottomBorderDistance", Transform.translateHwp2Office(shape.downSpace) <= 100 ? 0 : Transform.translateHwp2Office(shape.downSpace) - 100); // fill color setFillStyle(wOuterContext, frameProps, shape.fill); // insert text frame into document (order is important here) XText xText = wOuterContext.mTextCursor.getText(); xText.insertTextContent(wOuterContext.mTextCursor, xInternalFrame, false); if (wOuterContext.version >= 72) { TextContentAnchorType anchorType = (TextContentAnchorType) frameProps.getPropertyValue("AnchorType"); if (anchorType == TextContentAnchorType.AT_PARAGRAPH) { xText.insertString(wOuterContext.mTextCursor, " ", false); } } frameProps.setPropertyValue("FrameIsAutomaticHeight", false); // TextFrame을 그린 후에 automaticHeight를 조정해야.. // embedded rect는 autoHeight=true로 했었는데, 임시로 // false로 해보자. frameProps.setPropertyValue("TextVerticalAdjust", Transform.toTextVertAlign(shape.textVerAlign.ordinal())); wOuterContext.mTextCursor.gotoEnd(false); if (shape.paras != null) { WriterContext innerContext = new WriterContext(); innerContext.hwp = wOuterContext.hwp; innerContext.mContext = wOuterContext.mContext; innerContext.mDesktop = wOuterContext.mDesktop; innerContext.mMCF = wOuterContext.mMCF; innerContext.mMSF = wOuterContext.mMSF; innerContext.mMyDocument = wOuterContext.mMyDocument; innerContext.userHomeDir = wOuterContext.userHomeDir; innerContext.mText = xInternalFrame.getText(); innerContext.mTextCursor = innerContext.mText.createTextCursor(); // 외부Frame과 Ctrl의 크기를 비교한다. Ctrl의 크기가 크다면, 일부 보여지지 않아야 하므로, ZOrder를 낮게 수정한다. int maxCtrlWidth = 0, maxCtrlHeight = 0; try { maxCtrlWidth = shape.paras.stream().filter(para -> para.p != null && para.p.size() > 0) .flatMap(para -> para.p.stream()).filter(ctrl -> ctrl instanceof Ctrl_Common) .mapToInt(ctrl -> Integer.valueOf(((Ctrl_Common) ctrl).width)).max().getAsInt(); maxCtrlHeight = shape.paras.stream().filter(para -> para.p != null && para.p.size() > 0) .flatMap(para -> para.p.stream()).filter(ctrl -> ctrl instanceof Ctrl_Common) .mapToInt(ctrl -> Integer.valueOf(((Ctrl_Common) ctrl).height)).max().getAsInt(); } catch (NoSuchElementException e) { log.fine("Cannot get OptionalInt either maxCtrlWidth or maxCtrlHeight. " + e.getLocalizedMessage()); } for (HwpParagraph para : shape.paras) { // 테이블은 Frame 크기를 넘지 못하므로, 테이블 크기만큼 내부 Frame을 다시 만들어야 한다. // 다만, 큰 테이블이라도 보이는건 외부 Frame 만큼 보이도록 한다. HwpCallback callback = new HwpCallback(TableFrame.MAKE); if (sizeWidth < maxCtrlWidth || sizeHeight < maxCtrlHeight) { callback = new HwpCallback(TableFrame.MAKE_PART); } HwpRecurs.printParaRecurs(innerContext, wOuterContext, para, callback, step + 1); } HwpRecurs.removeLastParaBreak(innerContext.mTextCursor); if (shape.nGrp == 0) { ++autoNum; } } } catch (com.sun.star.uno.Exception e) { e.printStackTrace(); } catch (SkipDrawingException e) { e.printStackTrace(); } } private static void insertRECTANGLE(WriterContext wOuterContext, Ctrl_GeneralShape shape, int step, int shapeWidth, int shapeHeight) { try { Object xObj = wOuterContext.mMSF.createInstance("com.sun.star.drawing.RectangleShape"); XShape xShape = UnoRuntime.queryInterface(XShape.class, xObj); XTextContent xTextContentShape = (XTextContent) UnoRuntime.queryInterface(XTextContent.class, xObj); if (shape.nGrp == 0) { int sizeWidth = 0, sizeHeight = 0; if (shapeWidth <= 0 && shapeHeight <= 0) { sizeWidth = Math.abs(shape.curWidth); sizeHeight = Math.abs(shape.curHeight); if (sizeWidth==0 || sizeHeight==0) { sizeWidth = Math.abs(shape.width); sizeHeight = Math.abs(shape.height); } if (shape.rotat != 0) { Point2D ptSrc = new Point2D.Double(shape.curWidth, shape.curHeight); Point2D ptDst = Transform.rotateValue(shape.rotat, ptSrc); sizeWidth = (int) ptDst.getX(); sizeHeight = (int) ptDst.getY(); } } else { sizeWidth = shapeWidth; sizeHeight = shapeHeight; } xShape.setSize( new Size(Transform.translateHwp2Office(sizeWidth), Transform.translateHwp2Office(sizeHeight))); } else { // transform 방식으로 변경 (2024.01.28) // xShape.setSize(new Size(shape.width, shape.height)); } // anchor the text frame XPropertySet xPropsSet = UnoRuntime.queryInterface(XPropertySet.class, xShape); // insert text frame into document (order is important here) XText xText = wOuterContext.mTextCursor.getText(); if (shape.nGrp > 0) { xPropsSet.setPropertyValue("AnchorType", TextContentAnchorType.AT_PARAGRAPH); } else { xPropsSet.setPropertyValue("AnchorType", TextContentAnchorType.AT_PARAGRAPH); setPosition(xPropsSet, shape, 0, 0); } xText.insertTextContent(wOuterContext.mTextCursor, xTextContentShape, false); if (shape.nGrp > 0) { transform(xPropsSet, shape); } setWrapStyle(xPropsSet, shape); // ZOrder 설정해도 변경되지 않는다. 대신 gso Ctrl에서 꺼내올때 zOrder 순서로 가져와서 그린다. // xPropsSet.setPropertyValue("ZOrder", Integer.valueOf(shape.zOrder)); xPropsSet.setPropertyValue("LeftMargin", 0); xPropsSet.setPropertyValue("RightMargin", 0); xPropsSet.setPropertyValue("TopMargin", 0); xPropsSet.setPropertyValue("BottomMargin", 0); // fill color setFillStyle(wOuterContext, xPropsSet, shape.fill); setLineStyle(xPropsSet, shape); if (wOuterContext.version >= 72) { TextContentAnchorType anchorType = (TextContentAnchorType) xPropsSet.getPropertyValue("AnchorType"); if (anchorType == TextContentAnchorType.AT_PARAGRAPH) { xText.insertString(wOuterContext.mTextCursor, " ", false); } } wOuterContext.mTextCursor.gotoEnd(false); if (shape.paras != null) { xPropsSet.setPropertyValue("TextVerticalAdjust", Transform.toTextVertAlign(shape.textVerAlign.ordinal())); xPropsSet.setPropertyValue("TextHorizontalAdjust", TextHorizontalAdjust.CENTER); WriterContext innerContext = new WriterContext(); innerContext.hwp = wOuterContext.hwp; innerContext.mContext = wOuterContext.mContext; innerContext.mDesktop = wOuterContext.mDesktop; innerContext.mMCF = wOuterContext.mMCF; innerContext.mMSF = wOuterContext.mMSF; innerContext.mMyDocument = wOuterContext.mMyDocument; innerContext.userHomeDir = wOuterContext.userHomeDir; innerContext.mText = (XText) UnoRuntime.queryInterface(XText.class, xShape); innerContext.mTextCursor = innerContext.mText.createTextCursor(); // 외부Frame과 Ctrl의 크기를 비교한다. Ctrl의 크기가 크다면, 일부 보여지지 않아야 하므로, ZOrder를 낮게 수정한다. int maxCtrlWidth = 0, maxCtrlHeight = 0; try { maxCtrlWidth = shape.paras.stream().filter(para -> para.p != null && para.p.size() > 0) .flatMap(para -> para.p.stream()).filter(ctrl -> ctrl instanceof Ctrl_Common) .mapToInt(ctrl -> Integer.valueOf(((Ctrl_Common) ctrl).width)).max().getAsInt(); maxCtrlHeight = shape.paras.stream().filter(para -> para.p != null && para.p.size() > 0) .flatMap(para -> para.p.stream()).filter(ctrl -> ctrl instanceof Ctrl_Common) .mapToInt(ctrl -> Integer.valueOf(((Ctrl_Common) ctrl).height)).max().getAsInt(); } catch (NoSuchElementException e) { log.fine("Cannot get OptionalInt either maxCtrlWidth or maxCtrlHeight. " + e.getLocalizedMessage()); } for (int i=0; i 0) { xPropsSet.setPropertyValue("AnchorType", TextContentAnchorType.AT_PARAGRAPH); } else { xPropsSet.setPropertyValue("AnchorType", TextContentAnchorType.AT_PARAGRAPH); setPosition(xPropsSet, pic, 0, 0); } } setLineStyle(xPropsSet, pic); setWrapStyle(xPropsSet, pic); if (hasCaption) { xFrameText.insertTextContent(xFrameCursor, xTextContent, true); xFrameText.insertControlCharacter(xFrameCursor, ControlCharacter.PARAGRAPH_BREAK, false); } else { wOuterContext.mText.insertTextContent(wOuterContext.mTextCursor, xTextContent, true); if (wOuterContext.version >= 72) { TextContentAnchorType anchorType = (TextContentAnchorType) xPropsSet.getPropertyValue("AnchorType"); if (anchorType == TextContentAnchorType.AT_PARAGRAPH) { wOuterContext.mText.insertString(wOuterContext.mTextCursor, " ", false); } } } if (pic.nGrp > 0) { transform(xPropsSet, pic); } Fill imageFill = new Fill(); imageFill.fillType = 0x02; imageFill.mode = ImageFillType.TOTAL; imageFill.effect = 0; imageFill.binItemID = pic.binDataID; imageFill.alpha = 0; setFillStyle(wOuterContext, xPropsSet, imageFill); if (pic.nGrp == 0) { ++autoNum; } // 캡션 쓰기 if (hasCaption) { addCaptionString(wOuterContext, xFrameText, xFrameCursor, pic, step); } } catch (com.sun.star.uno.Exception e) { e.printStackTrace(); } catch (SkipDrawingException e) { e.printStackTrace(); } } private static void insertLINE(WriterContext wContext, Ctrl_ShapeLine shape, int step, int shapeWidth, int shapeHeight) { boolean hasCaption = shape.caption == null ? false : shape.caption.size() == 0 ? false : true; XTextFrame xFrame = null; XText xFrameText = null; XTextCursor xFrameCursor = null; try { if (hasCaption) { xFrame = makeOuterFrame(wContext, shape, false, step); // Frame 내부 Cursor 생성 xFrameText = xFrame.getText(); xFrameCursor = xFrameText.createTextCursor(); } Object xObj = wContext.mMSF.createInstance("com.sun.star.drawing.LineShape"); XTextContent xTextContentShape = (XTextContent) UnoRuntime.queryInterface(XTextContent.class, xObj); XShape xShape = (XShape) UnoRuntime.queryInterface(XShape.class, xObj); if (shape.nGrp == 0) { int sizeWidth = 0, sizeHeight = 0; if (shapeWidth <= 0 && shapeHeight <= 0) { double xScale = 1.0, yScale = 1.0; for (int i = 0; i < shape.matCnt; i++) { xScale *= shape.matrixSeq[i * 12 + 0]; yScale *= shape.matrixSeq[i * 12 + 4]; log.finest("[LINE] matCnt=" + i + ",matCnt=" + shape.matCnt + ",xSclae=" + xScale + ",yScale=" + yScale); } double xSize = (shape.endX - shape.startX) * xScale; double ySize = (shape.endY - shape.startY) * yScale; if (shape.rotat != 0) { Point2D ptSrc = new Point2D.Double(xSize, ySize); Point2D ptDst = Transform.rotateValue(shape.rotat, ptSrc); xSize = ptDst.getX(); ySize = ptDst.getY(); } sizeWidth = (int) xSize; sizeHeight = (int) ySize; } else { sizeWidth = shapeWidth; sizeHeight = shapeHeight; } Point aPos = new Point(0, 0); Size aSize = new Size(Transform.translateHwp2Office(sizeWidth), Transform.translateHwp2Office(sizeHeight)); xShape.setPosition(aPos); xShape.setSize(aSize); } else { xShape.setSize(new Size(shape.iniWidth, shape.iniHeight)); } XPropertySet xPropsSet = (XPropertySet) UnoRuntime.queryInterface(XPropertySet.class, xShape); if (hasCaption) { try { xPropsSet.setPropertyValue("AnchorType", TextContentAnchorType.AS_CHARACTER); } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("AnchorType has Exception"); } xPropsSet.setPropertyValue("VertOrient", VertOrientation.CENTER); // Top, Bottom, Center, fromBottom xPropsSet.setPropertyValue("VertOrientRelation", RelOrientation.TEXT_LINE); // Base line, Character, Row xPropsSet.setPropertyValue("HoriOrient", HoriOrientation.CENTER); // 0:NONE=From left xPropsSet.setPropertyValue("HoriOrientRelation", RelOrientation.PRINT_AREA); // 1:paragraph text area } else { if (shape.nGrp == 0) { xPropsSet.setPropertyValue("AnchorType", TextContentAnchorType.AT_PARAGRAPH); setPosition(xPropsSet, shape, 0, 0); } else { xPropsSet.setPropertyValue("AnchorType", TextContentAnchorType.AT_PARAGRAPH); } } setWrapStyle(xPropsSet, shape); setLineStyle(xPropsSet, shape); if (hasCaption) { XPropertySet frameProps = UnoRuntime.queryInterface(XPropertySet.class, xFrame); // 투명하게 해야 도형이 보인다. frameProps.setPropertyValue("FillTransparence", 100); xFrameText.insertTextContent(xFrameCursor, xTextContentShape, true); xFrameText.insertControlCharacter(xFrameCursor, ControlCharacter.PARAGRAPH_BREAK, false); } else { wContext.mText.insertTextContent(wContext.mTextCursor, xTextContentShape, true); if (shape.nGrp > 0) { transform(xPropsSet, shape); // workaround-LineShape-transform START // transform 으로 크기,이동,회전이 모두 변환되어야 할것 같은데, scale은 변하지 않음. 따라서 transform 이후에 // Size를 추가로 설정한다. if (xShape.getSize().Width / 10 == shape.iniWidth / 10 && xShape.getSize().Height / 10 == shape.iniHeight / 10) { if ((shape.rotat >= 45 && shape.rotat < 135) || shape.rotat >= 225 && shape.rotat < 315) { xShape.setSize(new Size( Transform.translateHwp2Office( shapeWidth == -1 && shapeHeight == -1 ? shape.curHeight : shapeHeight), Transform.translateHwp2Office( shapeWidth == -1 && shapeHeight == -1 ? shape.curWidth : shapeWidth))); } else { xShape.setSize(new Size( Transform.translateHwp2Office( shapeWidth == -1 && shapeHeight == -1 ? shape.curWidth : shapeWidth), Transform.translateHwp2Office( shapeWidth == -1 && shapeHeight == -1 ? shape.curHeight : shapeHeight))); } } // workaround-LineShape-transform END } if (step == 2 && wContext.version >= 72) { TextContentAnchorType anchorType = (TextContentAnchorType) xPropsSet.getPropertyValue("AnchorType"); if (anchorType == TextContentAnchorType.AT_PARAGRAPH) { wContext.mText.insertString(wContext.mTextCursor, " ", false); } } } setArrowStyle(xPropsSet, shape.lineHead, shape.lineHeadSz, true); setArrowStyle(xPropsSet, shape.lineTail, shape.lineTailSz, false); if (shape.nGrp == 0) { ++autoNum; } // 캡션 쓰기 if (hasCaption) { addCaptionString(wContext, xFrameText, xFrameCursor, shape, step); } } catch (com.sun.star.uno.Exception e) { e.printStackTrace(); } catch (SkipDrawingException e) { e.printStackTrace(); } } private static void insertELLIPSE(WriterContext wContext, Ctrl_ShapeEllipse ell, int step, int shapeWidth, int shapeHeight) { boolean hasCaption = ell.caption == null ? false : ell.caption.size() == 0 ? false : true; XTextFrame xFrame = null; XText xFrameText = null; XTextCursor xFrameCursor = null; try { if (hasCaption) { xFrame = makeOuterFrame(wContext, ell, false, step); // Frame 내부 Cursor 생성 xFrameText = xFrame.getText(); xFrameCursor = xFrameText.createTextCursor(); } Object xObj = wContext.mMSF.createInstance("com.sun.star.drawing.EllipseShape"); XTextContent xTextContentShape = (XTextContent) UnoRuntime.queryInterface(XTextContent.class, xObj); XShape xShape = (XShape) UnoRuntime.queryInterface(XShape.class, xObj); int sizeWidth = 0, sizeHeight = 0; if (shapeWidth <= 0 && shapeHeight <= 0) { sizeWidth = Math.abs(ell.curWidth); sizeHeight = Math.abs(ell.curHeight); if (sizeWidth==0 || sizeHeight==0) { sizeWidth = Math.abs(ell.width); sizeHeight = Math.abs(ell.height); } } else { sizeWidth = shapeWidth; sizeHeight = shapeHeight; } // 그릴 위치 Point aPos = new Point(0, 0); Size aSize = new Size(Transform.translateHwp2Office(sizeWidth), Transform.translateHwp2Office(sizeHeight)); xShape.setPosition(aPos); xShape.setSize(aSize); XPropertySet xPropSet = (XPropertySet) UnoRuntime.queryInterface(XPropertySet.class, xShape); if (hasCaption) { try { xPropSet.setPropertyValue("AnchorType", TextContentAnchorType.AS_CHARACTER); } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("AnchorType has Exception"); } xPropSet.setPropertyValue("VertOrient", VertOrientation.CENTER); // Top, Bottom, Center, fromBottom xPropSet.setPropertyValue("VertOrientRelation", RelOrientation.TEXT_LINE); // Base line, Character, Row xPropSet.setPropertyValue("HoriOrient", HoriOrientation.CENTER); // 0:NONE=From left xPropSet.setPropertyValue("HoriOrientRelation", RelOrientation.PRINT_AREA); // 1:paragraph text area // 투명하게 해야 도형이 보인다. // xPropSet.setPropertyValue("BackTransparent", RelOrientation.PRINT_AREA); // // 1:paragraph text area xPropSet.setPropertyValue("FillStyle", FillStyle.NONE); xPropSet.setPropertyValue("FillTransparence", 100); } else { setPosition(xPropSet, ell, ell.nGrp > 0 ? ell.xGrpOffset : 0, ell.nGrp > 0 ? ell.yGrpOffset : 0); } setWrapStyle(xPropSet, ell); setLineStyle(xPropSet, ell); xPropSet.setPropertyValue("CircleKind", CircleKind.FULL); if (hasCaption) { XPropertySet frameProps = UnoRuntime.queryInterface(XPropertySet.class, xFrame); // 투명하게 해야 도형이 보인다. frameProps.setPropertyValue("FillTransparence", 100); xFrameText.insertTextContent(xFrameCursor, xTextContentShape, false); xFrameText.insertControlCharacter(xFrameCursor, ControlCharacter.PARAGRAPH_BREAK, false); } else { wContext.mText.insertTextContent(wContext.mTextCursor, xTextContentShape, false); // workaround-LibreOffice7.2 START if (step == 2 && wContext.version >= 72) { TextContentAnchorType anchorType = (TextContentAnchorType) xPropSet.getPropertyValue("AnchorType"); if (anchorType == TextContentAnchorType.AT_PARAGRAPH) { wContext.mText.insertString(wContext.mTextCursor, " ", false); } } // workaround-LibreOffice7.2 END } setFillStyle(wContext, xPropSet, ell.fill); if (ell.nGrp == 0) { ++autoNum; } // 캡션 쓰기 if (hasCaption) { addCaptionString(wContext, xFrameText, xFrameCursor, ell, step); } } catch (com.sun.star.uno.Exception e) { e.printStackTrace(); } catch (SkipDrawingException e) { e.printStackTrace(); } } private static void insertPOLYGON(WriterContext wContext, Ctrl_ShapePolygon pol, int step, int shapeWidth, int shapeHeight) { // check below URL first before make the code to draw shapes. // https://wiki.openoffice.org/wiki/Documentation/DevGuide/Drawings/Shape_Types boolean hasParas = pol.paras == null ? false : pol.paras.size() == 0 ? false : true; boolean hasCaption = pol.caption == null ? false : pol.caption.size() == 0 ? false : true; XTextFrame xFrame = null; XText xFrameText = null; XTextCursor xFrameCursor = null; XPropertySet paraProps = null; try { if (hasParas || hasCaption) { xFrame = makeOuterFrame(wContext, pol, hasParas, step); // Frame 내부 Cursor 생성 xFrameText = xFrame.getText(); xFrameCursor = xFrameText.createTextCursor(); } Object xObj = wContext.mMSF.createInstance("com.sun.star.drawing.PolyPolygonShape"); XTextContent xTextContentShape = (XTextContent) UnoRuntime.queryInterface(XTextContent.class, xObj); XShape xShape = (XShape) UnoRuntime.queryInterface(XShape.class, xObj); // int sizeWidth = shapeWidth <= 0 ? pol.curWidth : shapeWidth; // int sizeHeight = shapeHeight <= 0 ? pol.curHeight : shapeHeight; int sizeWidth = 0, sizeHeight = 0; if (shapeWidth <= 0 && shapeHeight <= 0) { sizeWidth = Math.abs(pol.curWidth); sizeHeight = Math.abs(pol.curHeight); if (sizeWidth==0 || sizeHeight==0) { sizeWidth = Math.abs(pol.width); sizeHeight = Math.abs(pol.height); } } else { sizeWidth = shapeWidth; sizeHeight = shapeHeight; } // 그릴 위치 Point aPos = new Point(0, 0); Size aSize = new Size(Transform.translateHwp2Office(sizeWidth), Transform.translateHwp2Office(sizeHeight)); xShape.setPosition(aPos); xShape.setSize(aSize); XPropertySet xPropSet = (XPropertySet) UnoRuntime.queryInterface(XPropertySet.class, xShape); if (hasParas || hasCaption) { try { xPropSet.setPropertyValue("AnchorType", TextContentAnchorType.AS_CHARACTER); } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("AnchorType has Exception"); } xPropSet.setPropertyValue("VertOrient", VertOrientation.CENTER); // Top, Bottom, Center, fromBottom xPropSet.setPropertyValue("VertOrientRelation", RelOrientation.TEXT_LINE); // Base line, Character, Row xPropSet.setPropertyValue("HoriOrient", HoriOrientation.CENTER); // 0:NONE=From left xPropSet.setPropertyValue("HoriOrientRelation", RelOrientation.PRINT_AREA); // 1:paragraph text area // 투명하게 해야 도형이 보인다. // xPropSet.setPropertyValue("BackTransparent", RelOrientation.PRINT_AREA); // // 1:paragraph text area xPropSet.setPropertyValue("FillStyle", FillStyle.NONE); xPropSet.setPropertyValue("FillTransparence", 100); } else { setPosition(xPropSet, pol, pol.nGrp > 0 ? pol.xGrpOffset : 0, pol.nGrp > 0 ? pol.yGrpOffset : 0); } setWrapStyle(xPropSet, pol); setLineStyle(xPropSet, pol); PolyPolygonBezierCoords aCoords = new PolyPolygonBezierCoords(); int nPointCount = pol.nPoints; aCoords.Coordinates = new Point[1][]; aCoords.Flags = new PolygonFlags[1][]; Point[] pPolyPoints = new Point[nPointCount]; PolygonFlags[] pPolyFlags = new PolygonFlags[nPointCount]; for (int n = 0; n < nPointCount; n++) { pPolyPoints[n] = new Point(); pPolyPoints[n].X = Transform.translateHwp2Office(pol.points.get(n).x); pPolyPoints[n].Y = Transform.translateHwp2Office(pol.points.get(n).y); pPolyFlags[n] = PolygonFlags.NORMAL; } aCoords.Coordinates[0] = pPolyPoints; aCoords.Flags[0] = pPolyFlags; xPropSet.setPropertyValue("PolyPolygonBezier", aCoords); setFillStyle(wContext, xPropSet, pol.fill); if (hasParas || hasCaption) { XPropertySet frameProps = UnoRuntime.queryInterface(XPropertySet.class, xFrame); // 투명하게 해야 도형이 보인다. frameProps.setPropertyValue("FillTransparence", 100); } if (wContext.version >= 72) { TextContentAnchorType anchorType = (TextContentAnchorType) xPropSet.getPropertyValue("AnchorType"); if (((hasParas || hasCaption) && anchorType == TextContentAnchorType.AS_CHARACTER) || anchorType == TextContentAnchorType.AT_PARAGRAPH) { wContext.mText.insertString(wContext.mTextCursor, " ", false); } } // [21.11.24] "글상자 속성" 가진 개체 내에 문단 쓰기. if (hasParas) { WriterContext context2 = new WriterContext(); context2.mContext = wContext.mContext; context2.mDesktop = wContext.mDesktop; context2.mMCF = wContext.mMCF; context2.mMSF = wContext.mMSF; context2.mMyDocument = wContext.mMyDocument; context2.userHomeDir = wContext.userHomeDir; context2.mText = xFrameText; context2.mTextCursor = xFrameCursor; for (HwpParagraph para : pol.paras) { HwpCallback callback = new HwpCallback(TableFrame.MADE); HwpRecurs.printParaRecurs(context2, wContext, para, callback, step + 1); } // REMOVE last PARA_BREAK HwpRecurs.removeLastParaBreak(context2.mTextCursor); } if (pol.nGrp == 0) { ++autoNum; } // 캡션 쓰기. 글속성으로 처리하니 캡션을 없을듯 하나, 코드는 남겨놓음 if (hasCaption) { xFrameText.insertTextContent(xFrameCursor, xTextContentShape, false); xFrameText.insertControlCharacter(xFrameCursor, ControlCharacter.PARAGRAPH_BREAK, false); addCaptionString(wContext, xFrameText, xFrameCursor, pol, step); } } catch (com.sun.star.uno.Exception e) { e.printStackTrace(); } catch (SkipDrawingException e) { e.printStackTrace(); } } private static void insertCURVE(WriterContext wContext, Ctrl_ShapeCurve cur, int step) { String shapeString = "com.sun.star.drawing.OpenBezierShape"; if ((cur.fillType > 0)) { shapeString = "com.sun.star.drawing.ClosedBezierShape"; } boolean hasCaption = cur.caption == null ? false : cur.caption.size() == 0 ? false : true; XTextFrame xFrame = null; XText xFrameText = null; XTextCursor xFrameCursor = null; try { if (hasCaption) { xFrame = makeOuterFrame(wContext, cur, false, step); // Frame 내부 Cursor 생성 xFrameText = xFrame.getText(); xFrameCursor = xFrameText.createTextCursor(); } int sizeWidth = 0, sizeHeight = 0; sizeWidth = Math.abs(cur.curWidth); sizeHeight = Math.abs(cur.curHeight); if (sizeWidth==0 || sizeHeight==0) { sizeWidth = Math.abs(cur.width); sizeHeight = Math.abs(cur.height); } Object xObj = wContext.mMSF.createInstance(shapeString); XTextContent xTextContentShape = (XTextContent) UnoRuntime.queryInterface(XTextContent.class, xObj); XShape xShape = (XShape) UnoRuntime.queryInterface(XShape.class, xObj); // 그릴 위치 Point aPos = new Point(0, 0); Size aSize = new Size(Transform.translateHwp2Office(sizeWidth), Transform.translateHwp2Office(sizeHeight)); xShape.setPosition(aPos); xShape.setSize(aSize); XPropertySet xPropSet = (XPropertySet) UnoRuntime.queryInterface(XPropertySet.class, xShape); if (hasCaption) { try { xPropSet.setPropertyValue("AnchorType", TextContentAnchorType.AS_CHARACTER); } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("AnchorType has Exception"); } xPropSet.setPropertyValue("VertOrient", VertOrientation.CENTER); // Top, Bottom, Center, fromBottom xPropSet.setPropertyValue("VertOrientRelation", RelOrientation.TEXT_LINE); // Base line, Character, Row xPropSet.setPropertyValue("HoriOrient", HoriOrientation.CENTER); // 0:NONE=From left xPropSet.setPropertyValue("HoriOrientRelation", RelOrientation.PRINT_AREA); // 1:paragraph text area // 투명하게 해야 도형이 보인다. // xPropSet.setPropertyValue("BackTransparent", RelOrientation.PRINT_AREA); // // 1:paragraph text area xPropSet.setPropertyValue("FillStyle", FillStyle.NONE); xPropSet.setPropertyValue("FillTransparence", 100); } else { setPosition(xPropSet, cur, cur.nGrp > 0 ? cur.xGrpOffset : 0, cur.nGrp > 0 ? cur.yGrpOffset : 0); } setWrapStyle(xPropSet, cur); setLineStyle(xPropSet, cur); PolyPolygonBezierCoords aCoords = new PolyPolygonBezierCoords(); // 시작점*2 + 끝점*2 + 중간점*3. [N C] [C N C] [C N C] [C N C] [C N]. 컨트롤 Point는 pair로 // 와야 하며, pair 전후에 Normal Point가 있어야 한다. int nPointCount = 4 + (cur.nPoints - 2) * 3; aCoords.Coordinates = new Point[1][]; aCoords.Flags = new PolygonFlags[1][]; Point[] pPolyPoints = new Point[nPointCount]; PolygonFlags[] pPolyFlags = new PolygonFlags[nPointCount]; // 아래 CONTROL Point 계산은 출처가 있는 것이 아니고, 직접 작성한 계산방법이다. // 더 좋은 계산법이 있다면 아래의 코드를 교체해도 무방하다. double nDiv = 6.8; // 이 값을 7전후로 맞춰야 HWP와 유사한 곡선 비율이 나온다. for (int i = 0, n = 0; i < cur.nPoints; i++, n += 3) { if (i == 0) { pPolyPoints[n] = new Point(); pPolyPoints[n].X = Transform.translateHwp2Office(cur.points[i].x); pPolyPoints[n].Y = Transform.translateHwp2Office(cur.points[i].y); pPolyFlags[n] = PolygonFlags.NORMAL; pPolyPoints[n + 1] = new Point(); pPolyPoints[n + 1].X = Transform.translateHwp2Office(cur.points[i].x); pPolyPoints[n + 1].Y = Transform.translateHwp2Office(cur.points[i].y); pPolyFlags[n + 1] = PolygonFlags.CONTROL; } else if (i == cur.nPoints - 1) { pPolyPoints[n - 1] = new Point(); pPolyPoints[n - 1].X = Transform.translateHwp2Office(cur.points[i].x); pPolyPoints[n - 1].Y = Transform.translateHwp2Office(cur.points[i].y); pPolyFlags[n - 1] = PolygonFlags.CONTROL; pPolyPoints[n] = new Point(); pPolyPoints[n].X = Transform.translateHwp2Office(cur.points[i].x); pPolyPoints[n].Y = Transform.translateHwp2Office(cur.points[i].y); pPolyFlags[n] = PolygonFlags.NORMAL; } else { // CONTROL Point before NORMAL Point double atan1 = Math.atan2(cur.points[i].y - cur.points[i - 1].y, cur.points[i - 1].x - cur.points[i].x); int angle1 = (int) (atan1 * 180 / Math.PI); // NORMAL Point에 들어온 각도 double atan2 = Math.atan2(cur.points[i].y - cur.points[i + 1].y, cur.points[i + 1].x - cur.points[i].x); int angle2 = (int) (atan2 * 180 / Math.PI); // NORMAL Point에서 나가는 각도 double angle3 = angle1 - (angle1 - angle2) / 2; // 들어온 각도와 나간 각도의 중간각 double distance1 = Math.sqrt(Math.pow(cur.points[i].y - cur.points[i - 1].y, 2) + Math.pow(cur.points[i - 1].x - cur.points[i].x, 2)); double distance2 = Math.sqrt(Math.pow(cur.points[i].y - cur.points[i + 1].y, 2) + Math.pow(cur.points[i + 1].x - cur.points[i].x, 2)); double distance = Math.max(distance1, distance2); // 양쪽 CONTROL Point 벌어짐은 긴 쪽을 기준으로 같게 한다. // nDiv = 5+2/(1+Math.exp(1-0.0001*distance)); // nDiv를 가변적으로 조정하는 시그모이드 함수 double signX = Math.signum(Math.cos(Math.toRadians(angle1 > angle3 ? angle3 + 90 : angle3 - 90))); // 접선의 // 각도로 // Point // X좌표값을 // 더할지 // 뺄지 // 결정하는 // 부호 double signY = -1 * Math.signum(Math.sin(Math.toRadians(angle1 > angle3 ? angle3 + 90 : angle3 - 90)));// 접선의 // 각도로 // Point // Y좌표값을 // 더할지 // 뺄지 // 결정하는 // 부호 double deltaY = distance / nDiv * Math.abs(Math.cos(Math.toRadians(angle3))) * signY; // 접점에서 이격된 Y축 // 거리 double deltaX = distance / nDiv * Math.abs(Math.sin(Math.toRadians(angle3))) * signX; // 접점에서 이격된 X축 // 거리 int controlX = cur.points[i].x + (int) deltaX; // CONTROL Point의 x좌표 int controlY = cur.points[i].y + (int) deltaY; // CONTROL Point의 y좌표 pPolyPoints[n - 1] = new Point(); pPolyPoints[n - 1].X = Transform.translateHwp2Office(controlX); pPolyPoints[n - 1].Y = Transform.translateHwp2Office(controlY); pPolyFlags[n - 1] = PolygonFlags.CONTROL; // NORMAL Point pPolyPoints[n] = new Point(); pPolyPoints[n].X = Transform.translateHwp2Office(cur.points[i].x); pPolyPoints[n].Y = Transform.translateHwp2Office(cur.points[i].y); pPolyFlags[n] = PolygonFlags.NORMAL; // CONTROL Point after NORMAL Point signX = Math.signum(Math.cos(Math.toRadians(angle2 > angle3 ? angle3 + 90 : angle3 - 90))); signY = -1 * Math.signum(Math.sin(Math.toRadians(angle2 > angle3 ? angle3 + 90 : angle3 - 90))); deltaY = distance / nDiv * Math.abs(Math.cos(Math.toRadians(angle3))) * signY; deltaX = distance / nDiv * Math.abs(Math.sin(Math.toRadians(angle3))) * signX; controlX = cur.points[i].x + (int) deltaX; controlY = cur.points[i].y + (int) deltaY; pPolyPoints[n + 1] = new Point(); pPolyPoints[n + 1].X = Transform.translateHwp2Office(controlX); pPolyPoints[n + 1].Y = Transform.translateHwp2Office(controlY); pPolyFlags[n + 1] = PolygonFlags.CONTROL; } } aCoords.Coordinates[0] = pPolyPoints; aCoords.Flags[0] = pPolyFlags; xPropSet.setPropertyValue("PolyPolygonBezier", aCoords); setFillStyle(wContext, xPropSet, cur.fill); if (cur.nGrp == 0) { ++autoNum; } if (hasCaption) { XPropertySet frameProps = UnoRuntime.queryInterface(XPropertySet.class, xFrame); // 투명하게 해야 도형이 보인다. frameProps.setPropertyValue("FillTransparence", 100); xFrameText.insertTextContent(xFrameCursor, xTextContentShape, false); xFrameText.insertControlCharacter(xFrameCursor, ControlCharacter.PARAGRAPH_BREAK, false); } else { wContext.mText.insertTextContent(wContext.mTextCursor, xTextContentShape, false); // workaround-LibreOffice7.2 START if (step == 2 && wContext.version >= 72) { TextContentAnchorType anchorType = (TextContentAnchorType) xPropSet.getPropertyValue("AnchorType"); if (anchorType == TextContentAnchorType.AT_PARAGRAPH) { wContext.mText.insertString(wContext.mTextCursor, " ", false); } } // workaround-LibreOffice7.2 END } // 캡션 쓰기 if (hasCaption) { addCaptionString(wContext, xFrameText, xFrameCursor, cur, step); } } catch (com.sun.star.uno.Exception e) { e.printStackTrace(); } catch (SkipDrawingException e) { e.printStackTrace(); } } private static void insertARC(WriterContext wOuterContext, Ctrl_ShapeArc arc, int step, int shapeWidth, int shapeHeight) { boolean hasCaption = arc.caption == null ? false : arc.caption.size() == 0 ? false : true; XTextFrame xInternalFrame = null; XText xInternalFrameText = null; XTextCursor xInternalFrameCursor = null; try { if (hasCaption) { xInternalFrame = makeOuterFrame(wOuterContext, arc, false, step); // Frame 내부 Cursor 생성 xInternalFrameText = xInternalFrame.getText(); xInternalFrameCursor = xInternalFrameText.createTextCursor(); } Object xObj = wOuterContext.mMSF.createInstance("com.sun.star.drawing.EllipseShape"); XShape xShape = (XShape) UnoRuntime.queryInterface(XShape.class, xObj); XTextContent xTextContentShape = (XTextContent) UnoRuntime.queryInterface(XTextContent.class, xObj); XPropertySet xPropSet = (XPropertySet) UnoRuntime.queryInterface(XPropertySet.class, xShape); int sizeWidth = 0, sizeHeight = 0; if (shapeWidth <= 0 && shapeHeight <= 0) { sizeWidth = Math.abs(arc.curWidth); sizeHeight = Math.abs(arc.curHeight); if (sizeWidth==0 || sizeHeight==0) { sizeWidth = Math.abs(arc.width); sizeHeight = Math.abs(arc.height); } } else { sizeWidth = shapeWidth; sizeHeight = shapeHeight; } double atan1 = Math.atan2(arc.centerY - arc.axixY1, arc.axixX1 - arc.centerX); int angle1 = (int) (atan1 * 180 / Math.PI); angle1 = angle1 >= 0 ? angle1 : 360 + angle1; double atan2 = Math.atan2(arc.centerY - arc.axixY2, arc.axixX2 - arc.centerX); int angle2 = (int) (atan2 * 180 / Math.PI); angle2 = angle2 >= 0 ? angle2 : 360 + angle2; // 그리는 좌표(HoriOrientPosition,VertOrientPosition) 변경을 위한 부분 - 시작 int xOffset = 0; int yOffset = 0; double radius = Math.sqrt(Math.pow((arc.centerY - arc.axixY1) * arc.matrixSeq[4], 2) + Math.pow((arc.axixX1 - arc.centerX) * arc.matrixSeq[0], 2)); int startQuadrant = angle1 >= 270 ? 4 : angle1 >= 180 ? 3 : angle1 >= 90 ? 2 : 1; int endQuadrant = angle2 >= 270 ? 4 : angle2 >= 180 ? 3 : angle2 >= 90 ? 2 : 1; if (startQuadrant > endQuadrant) { endQuadrant += 4; // 시각각도가 끝각도보다 크다면, 사분면 차지여부를 알기 위해서 한바뀌 돌려야 함. } List xyPlot = IntStream.range(startQuadrant, endQuadrant).mapToObj(i -> Integer.valueOf(i)) .collect(Collectors.toList()); if (xyPlot.contains(2)) { // 2사분면 위치함. x.y 조정 필요 없음 } else { if (xyPlot.contains(1)) { // 1사분면 위치함. x 위치 조정 xOffset = Transform.translateHwp2Office((int) -radius); } if (xyPlot.contains(3)) { // 3사분면 위치함. y 위치 조정 yOffset = Transform.translateHwp2Office((int) -radius); } if (xyPlot.size() == 1 && xyPlot.contains(4)) { // 4사분면 위치함. x,y 모두 위치 조정 xOffset = Transform.translateHwp2Office((int) -radius); yOffset = Transform.translateHwp2Office((int) -radius); } } // 그리는 좌표(HoriOrientPosition,VertOrientPosition) 변경을 위한 부분 - 끝 // 그릴 위치 Point aPos = new Point(0, 0); // ARC의 경우, HWP의 ARC보다 작은 크기로 나타난다. 사이즈를 재계산할 필요가 있다. // 삼각함수를 이용하여 가로,세로 길이를 구하고, 전체 원지름 대비 비율로 사이즈를 재계산할 수 있겠다. // 그런 후에 xShape.setSize(aSize) 를 호출하면 될것으로 보인다. 당장은 2배로 부풀려서 그리도록 한다. Size aSize = new Size(Transform.translateHwp2Office(sizeWidth) * 2, Transform.translateHwp2Office(sizeHeight) * 2); xShape.setPosition(aPos); xShape.setSize(aSize); if (hasCaption) { try { xPropSet.setPropertyValue("AnchorType", TextContentAnchorType.AS_CHARACTER); } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("AnchorType has Exception"); } xPropSet.setPropertyValue("VertOrient", VertOrientation.CENTER); // Top, Bottom, Center, fromBottom xPropSet.setPropertyValue("VertOrientRelation", RelOrientation.TEXT_LINE); // Base line, Character, Row xPropSet.setPropertyValue("HoriOrient", HoriOrientation.CENTER); // 0:NONE=From left xPropSet.setPropertyValue("HoriOrientRelation", RelOrientation.PRINT_AREA); // 1:paragraph text area // 투명하게 해야 도형이 보인다. // xPropSet.setPropertyValue("BackTransparent", RelOrientation.PRINT_AREA); // // 1:paragraph text area xPropSet.setPropertyValue("FillStyle", FillStyle.NONE); xPropSet.setPropertyValue("FillTransparence", 100); } else { setPosition(xPropSet, arc, xOffset, yOffset); // 호는 추가적으로 그리는 위치를 shift 해야 한다. xPropSet.setPropertyValue("VertOrientPosition", Transform.translateHwp2Office(((int) (arc.yGrpOffset * arc.matrixSeq[4]))) + yOffset); xPropSet.setPropertyValue("HoriOrientPosition", Transform.translateHwp2Office(((int) (arc.xGrpOffset * arc.matrixSeq[0]))) + xOffset); } setWrapStyle(xPropSet, arc); setLineStyle(xPropSet, arc); setFillStyle(wOuterContext, xPropSet, arc.fill); xPropSet.setPropertyValue("CircleStartAngle", angle1 * 100); xPropSet.setPropertyValue("CircleEndAngle", angle2 * 100); switch (arc.type) { case NORMAL: xPropSet.setPropertyValue("CircleKind", CircleKind.ARC); break; case PIE: xPropSet.setPropertyValue("CircleKind", CircleKind.SECTION); break; case CHORD: xPropSet.setPropertyValue("CircleKind", CircleKind.CUT); break; } if (hasCaption) { XPropertySet frameProps = UnoRuntime.queryInterface(XPropertySet.class, xInternalFrame); // 투명하게 해야 도형이 보인다. frameProps.setPropertyValue("FillTransparence", 100); xInternalFrameText.insertTextContent(xInternalFrameCursor, xTextContentShape, false); xInternalFrameText.insertControlCharacter(xInternalFrameCursor, ControlCharacter.PARAGRAPH_BREAK, false); } else { wOuterContext.mText.insertTextContent(wOuterContext.mTextCursor, xTextContentShape, false); if (step == 2 && wOuterContext.version >= 72) { TextContentAnchorType anchorType = (TextContentAnchorType) xPropSet.getPropertyValue("AnchorType"); if (anchorType == TextContentAnchorType.AT_PARAGRAPH) { wOuterContext.mText.insertString(wOuterContext.mTextCursor, " ", false); } } } if (arc.nGrp == 0) { ++autoNum; } // 캡션 쓰기 if (hasCaption) { addCaptionString(wOuterContext, xInternalFrameText, xInternalFrameCursor, arc, step); } } catch (com.sun.star.uno.Exception e) { e.printStackTrace(); } catch (SkipDrawingException e) { e.printStackTrace(); } } static XTextFrame makeOuterFrame(WriterContext wContext, Ctrl_GeneralShape shape, boolean fixedSize, int step) throws SkipDrawingException, Exception { XTextFrame xFrame = null; Object oFrame = wContext.mMSF.createInstance("com.sun.star.text.TextFrame"); xFrame = (XTextFrame) UnoRuntime.queryInterface(XTextFrame.class, oFrame); if (xFrame == null) { log.severe("Could not create a text frame"); return xFrame; } XShape tfShape = UnoRuntime.queryInterface(XShape.class, xFrame); tfShape.setSize( new Size(Transform.translateHwp2Office(shape.width), Transform.translateHwp2Office(shape.height))); XPropertySet frameProps = UnoRuntime.queryInterface(XPropertySet.class, xFrame); setPosition(frameProps, shape, 0, 0); setWrapStyle(frameProps, shape); BorderLine2 frameBorder = new BorderLine2(); frameBorder.Color = 0x000000; frameBorder.LineStyle = BorderLineStyle.NONE; frameBorder.InnerLineWidth = 0; frameBorder.OuterLineWidth = 0; frameBorder.LineDistance = 0; frameBorder.LineWidth = 0; frameProps.setPropertyValue("TopBorder", frameBorder); frameProps.setPropertyValue("BottomBorder", frameBorder); frameProps.setPropertyValue("LeftBorder", frameBorder); frameProps.setPropertyValue("RightBorder", frameBorder); // margin 0으로 frameProps.setPropertyValue("LeftMargin", 0); frameProps.setPropertyValue("RightMargin", 0); frameProps.setPropertyValue("TopMargin", 0); frameProps.setPropertyValue("BottomMargin", 0); // 안쪽여백을 0으로... frameProps.setPropertyValue("BorderDistance", 0); // TextDirection. if (shape.maxTxtWidth != shape.curWidth && shape.maxTxtWidth == shape.curHeight) { frameProps.setPropertyValue("WritingMode", WritingMode2.TB_RL); } XText xText = wContext.mTextCursor.getText(); xText.insertTextContent(wContext.mTextCursor, xFrame, false); if (wContext.version >= 72) { TextContentAnchorType anchorType = (TextContentAnchorType) frameProps.getPropertyValue("AnchorType"); if (anchorType == TextContentAnchorType.AT_PARAGRAPH) { xText.insertString(wContext.mTextCursor, " ", false); } } // Transparency 100%로 설정한다. frameProps.setPropertyValue("FillStyle", FillStyle.NONE); frameProps.setPropertyValue("FillTransparence", 100); // TextFrame을 그린 후에 automaticHeight를 조정해야.. if (fixedSize) { frameProps.setPropertyValue("WidthType", SizeType.FIX); frameProps.setPropertyValue("TextVerticalAdjust", TextVerticalAdjust.BLOCK); frameProps.setPropertyValue("FrameIsAutomaticHeight", false); } else { frameProps.setPropertyValue("WidthType", SizeType.FIX); frameProps.setPropertyValue("TextVerticalAdjust", TextVerticalAdjust.TOP); frameProps.setPropertyValue("FrameIsAutomaticHeight", false); } wContext.mTextCursor.gotoEnd(false); return xFrame; } static void addCaptionString(WriterContext wContext, XText xFrameText, XTextCursor xFrameCursor, Ctrl_GeneralShape shape, int step) { XParagraphCursor paraCursor = UnoRuntime.queryInterface(XParagraphCursor.class, xFrameCursor); XPropertySet paraProps = UnoRuntime.queryInterface(XPropertySet.class, paraCursor); List capStr = new ArrayList(); short[] charShapeID = new short[1]; Optional ctrlOp = shape.caption.stream().filter(c -> c.p != null).flatMap(c -> c.p.stream()).findFirst(); if (ctrlOp.isPresent()) { if (ctrlOp.get() instanceof ParaText) { charShapeID[0] = (short) ((ParaText) ctrlOp.get()).charShapeId; } else if (ctrlOp.get() instanceof Ctrl_Character) { charShapeID[0] = (short) ((Ctrl_Character) ctrlOp.get()).charShapeId; } } HwpCallback callback = new HwpCallback() { @Override public void onAutoNumber(Ctrl_AutoNumber autoNumber, int paraStyleID, int paraShapeID) { capStr.add(Integer.toString(autoNum)); }; @Override public boolean onTab(String info) { capStr.add("\t"); return true; }; @Override public boolean onText(String content, int charShapeId, int charPos, boolean append) { capStr.add(content); charShapeID[0] = (short) charShapeId; return true; } @Override public boolean onParaBreak() { capStr.add("\r"); return true; } }; HwpRecurs.printParaRecurs(wContext, wContext, shape.caption.get(0), callback, 2); if (capStr.size() > 0 && capStr.get(capStr.size() - 1).equals("\r")) { // 마지막이 PARA_BREAK라면 출력하지 않음. capStr.remove(capStr.size() - 1); } HwpRecord_ParaShape captionParaShape = wContext.getParaShape(shape.caption.get(0).paraShapeID); String styleName = ConvPara.getStyleName((int) shape.caption.get(0).paraStyleID); // short charShapeID = // ConvUtil.selectCharShapeID(shape.caption.get(0).charShapes, 0); HwpRecord_CharShape captionCharShape = wContext.getCharShape(charShapeID[0]); try { paraProps.setPropertyValue("ParaStyleName", styleName); ConvPara.setParagraphProperties(paraProps, captionParaShape, wContext.getDocInfo().compatibleDoc, ConvPara.PARA_SPACING); HwpRecord_BorderFill borderFill = wContext.getBorderFill(captionCharShape.borderFillIDRef); ConvPara.setCharacterProperties(paraProps, captionCharShape, borderFill, step); paraProps.setPropertyValue("ParaTopMargin", Transform.translateHwp2Office(shape.captionSpacing)); for (String cap : capStr) { xFrameText.insertString(xFrameCursor, cap, false); } } catch (Exception e) { e.printStackTrace(); } } private static void setPosition(XPropertySet xProps, Ctrl_GeneralShape shape, int xGrpOffset, int yGrpOffset) throws SkipDrawingException { int xOffsetToAdd = Transform.translateHwp2Office(xGrpOffset); int yOffsetToAdd = Transform.translateHwp2Office(yGrpOffset); setPositionLO(xProps, shape, xOffsetToAdd, yOffsetToAdd); } private static void setPositionLO(XPropertySet xProps, Ctrl_GeneralShape shape, int xOffsetToAdd, int yOffsetToAdd) throws SkipDrawingException { int posX = 0; int posY = 0; Page page = ConvPage.getCurrentPage().page; try { if (shape.treatAsChar == true) { try { xProps.setPropertyValue("AnchorType", TextContentAnchorType.AS_CHARACTER); } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("AnchorType has Exception"); } xProps.setPropertyValue("VertOrient", VertOrientation.CHAR_CENTER); // Top, Bottom, Center, fromBottom xProps.setPropertyValue("VertOrientRelation", RelOrientation.PRINT_AREA); // Base line, Character, Row xProps.setPropertyValue("HoriOrient", HoriOrientation.CENTER); // 0:NONE=From left xProps.setPropertyValue("HoriOrientRelation", RelOrientation.PRINT_AREA); // 1:paragraph text area } else { if (shape.vertRelTo == null) { xProps.setPropertyValue("AnchorType", TextContentAnchorType.AT_PARAGRAPH); xProps.setPropertyValue("VertOrientRelation", RelOrientation.PRINT_AREA); xProps.setPropertyValue("VertOrient", VertOrientation.NONE); xProps.setPropertyValue("VertOrientPosition", yOffsetToAdd); } else { switch (shape.vertRelTo) { case PAPER: // Anchor to Page // 그림에서 AnchorType을 AT_PAGE로 줄때 crash 발생 if (!(shape instanceof Ctrl_ShapePic)) { try { xProps.setPropertyValue("AnchorType", TextContentAnchorType.AT_PARAGRAPH); } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("AnchorType has Exception"); } } switch (shape.vertAlign) { case TOP: xProps.setPropertyValue("VertOrientRelation", RelOrientation.PAGE_FRAME); // 7:EntirePage, // 8:PageTextArea if (shape.vertOffset == 0) { xProps.setPropertyValue("VertOrient", VertOrientation.TOP); // 1:Top, 2:Bottom, // 2:Center, 0:NONE(From // top) } else { xProps.setPropertyValue("VertOrient", VertOrientation.NONE); // 0:NONE=From top // page상단으로부터 frame상단까지의 offset posY = Transform.translateHwp2Office(shape.vertOffset); if (posY < 0 || posY + Transform.translateHwp2Office(shape.height) > Transform .translateHwp2Office(page.height)) { throw new SkipDrawingException(); } xProps.setPropertyValue("VertOrientPosition", posY + yOffsetToAdd); } break; case CENTER: xProps.setPropertyValue("VertOrientRelation", RelOrientation.PAGE_FRAME); // 7:EntirePage, // 8:PageTextArea if (shape.vertOffset == 0) { xProps.setPropertyValue("VertOrient", VertOrientation.CENTER); // 1:Top, 2:Bottom, // 2:Center, 0:NONE(From // top) } else { xProps.setPropertyValue("VertOrient", VertOrientation.NONE); // 0:NONE=From top // 중간지점에서 frame 중심까지의 offset -> page상단부터의 frame상단까지 offset으로 계산 posY = (Transform.translateHwp2Office(page.height) - Transform.translateHwp2Office(shape.height)) / 2 + Transform.translateHwp2Office(shape.vertOffset); if (posY < 0 || posY + Transform.translateHwp2Office(shape.height) > Transform .translateHwp2Office(page.height)) { throw new SkipDrawingException(); } xProps.setPropertyValue("VertOrientPosition", posY + yOffsetToAdd); } break; case BOTTOM: xProps.setPropertyValue("VertOrientRelation", RelOrientation.PAGE_FRAME); // 7:EntirePage, // 8:PageTextArea if (shape.vertOffset == 0) { xProps.setPropertyValue("VertOrient", VertOrientation.BOTTOM); // 1:Top, 2:Bottom, // 2:Center, 0:NONE(From // top) } else { xProps.setPropertyValue("VertOrient", VertOrientation.NONE); // 0:NONE=From top // page하단에서 frame 하단까지의 offset을 -> page상단부터의 frame상단까지 offset으로 계산 posY = Transform.translateHwp2Office(page.height) - Transform.translateHwp2Office(shape.height) - Transform.translateHwp2Office(shape.vertOffset); if (posY < 0 || posY + Transform.translateHwp2Office(shape.height) > Transform .translateHwp2Office(page.height)) { throw new SkipDrawingException(); } xProps.setPropertyValue("VertOrientPosition", posY + yOffsetToAdd); } break; } break; case PAGE: // 그림에서 AnchorType을 AT_PAGE로 줄때 crash 발생 if (!(shape instanceof Ctrl_ShapePic)) { try { xProps.setPropertyValue("AnchorType", TextContentAnchorType.AT_PARAGRAPH); } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("AnchorType has Exception"); } } switch (shape.vertAlign) { case TOP: xProps.setPropertyValue("VertOrientRelation", RelOrientation.PAGE_PRINT_AREA); // 7:EntirePage, // 8:PageTextArea if (shape.vertOffset == 0) { xProps.setPropertyValue("VertOrient", VertOrientation.TOP); // 1:Top, 2:Bottom, // 2:Center, 0:NONE(From // top) } else { xProps.setPropertyValue("VertOrient", VertOrientation.NONE); // 0:NONE=From top // page상단으로부터 frame상단까지의 offset posY = Transform.translateHwp2Office(shape.vertOffset); if (posY + Transform.translateHwp2Office(page.marginTop) < 0 || posY + Transform.translateHwp2Office(shape.height) > Transform .translateHwp2Office(page.height)) { throw new SkipDrawingException(); } xProps.setPropertyValue("VertOrientPosition", posY + yOffsetToAdd); } break; case CENTER: xProps.setPropertyValue("VertOrientRelation", RelOrientation.PAGE_PRINT_AREA); // 7:EntirePage, // 8:PageTextArea if (shape.vertOffset == 0) { xProps.setPropertyValue("VertOrient", VertOrientation.CENTER); // 1:Top, 2:Bottom, // 2:Center, 0:NONE(From // top) } else { xProps.setPropertyValue("VertOrient", VertOrientation.NONE); // 0:NONE=From top // 중간지점에서 frame 중심까지의 offset -> page상단부터의 frame상단까지 offset으로 계산 int pageHeight = Transform.translateHwp2Office(page.height) - Transform.translateHwp2Office(page.marginTop) - Transform.translateHwp2Office(page.marginBottom); posY = (pageHeight - Transform.translateHwp2Office(shape.height)) / 2; posY += Transform.translateHwp2Office(shape.vertOffset); if (posY < 0 || posY + Transform.translateHwp2Office(shape.height) > Transform .translateHwp2Office(page.height)) { throw new SkipDrawingException(); } xProps.setPropertyValue("VertOrientPosition", posY + yOffsetToAdd); } break; case BOTTOM: xProps.setPropertyValue("VertOrientRelation", RelOrientation.PAGE_PRINT_AREA); // 7:EntirePage, // 8:PageTextArea if (shape.vertOffset == 0) { xProps.setPropertyValue("VertOrient", VertOrientation.BOTTOM); // 1:Top, 2:Bottom, // 2:Center, 0:NONE(From // top) } else { xProps.setPropertyValue("VertOrient", VertOrientation.NONE); // 0:NONE=From top // 쪽 하단에서 frame 하단까지의 offset을 -> 쪽 상단부터의 frame상단까지 offset으로 계산 int pageHeight = Transform.translateHwp2Office(page.height) - Transform.translateHwp2Office(page.marginTop) - Transform.translateHwp2Office(page.marginBottom); posY = pageHeight - Transform.translateHwp2Office(shape.height) - Transform.translateHwp2Office(shape.vertOffset); if (posY < 0 || posY + Transform.translateHwp2Office(shape.height) > Transform .translateHwp2Office(page.height)) { throw new SkipDrawingException(); } xProps.setPropertyValue("VertOrientPosition", posY + yOffsetToAdd); } break; } break; case PARA: try { xProps.setPropertyValue("AnchorType", TextContentAnchorType.AT_PARAGRAPH); } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("AnchorType has Exception"); } switch (shape.vertAlign) { case TOP: xProps.setPropertyValue("VertOrientRelation", RelOrientation.PRINT_AREA); // 7:EntirePage, // 8:PageTextArea if (shape.vertOffset == 0) { xProps.setPropertyValue("VertOrient", VertOrientation.TOP); // 1:Top, 2:Bottom, // 2:Center, 0:NONE(From // top) } else { xProps.setPropertyValue("VertOrient", VertOrientation.NONE); // 0:NONE=From top // para상단으로부터 frame상단까지의 offset posY = Math.max(0, Transform.translateHwp2Office(shape.vertOffset)); if (/* posY+Transform.translateHwp2Office(page.marginTop)<0 || */ posY + Transform.translateHwp2Office(shape.height) > Transform .translateHwp2Office(page.height)) { throw new SkipDrawingException(); } xProps.setPropertyValue("VertOrientPosition", posY + yOffsetToAdd); } break; } break; } } if (shape.horzRelTo == null) { xProps.setPropertyValue("HoriOrientRelation", RelOrientation.PRINT_AREA); xProps.setPropertyValue("HoriOrient", HoriOrientation.NONE); xProps.setPropertyValue("HoriOrientPosition", xOffsetToAdd); } else { switch (shape.horzRelTo) { case PAPER: switch (shape.horzAlign) { case LEFT: // LEFT xProps.setPropertyValue("HoriOrientRelation", RelOrientation.PAGE_FRAME); // 7:EntirePage, // 8:PageTextArea if (shape.horzOffset == 0) { xProps.setPropertyValue("HoriOrient", HoriOrientation.LEFT); // 1:Top, 2:Bottom, // 2:Center, 0:NONE(From // top) } else { xProps.setPropertyValue("HoriOrient", HoriOrientation.NONE); // 0:NONE=From top // page상단으로부터 frame상단까지의 offset posX = Transform.translateHwp2Office(shape.horzOffset); int leftMargin = Transform.translateHwp2Office(page.marginLeft); int shapeWidth = Transform.translateHwp2Office(shape.width); int pageWidth = Transform .translateHwp2Office(page.landscape ? page.height : page.width); // 보이는 부분이 50% 넘으면 보이도록 수정 if ((posX + xOffsetToAdd < 0 && (posX + xOffsetToAdd + shapeWidth / 2) < 0) || (posX + xOffsetToAdd + shapeWidth > pageWidth && (posX + xOffsetToAdd + shapeWidth / 2) > pageWidth)) { log.fine("posX=" + posX + ", shapeWidth=" + shapeWidth + ", pageLeftMargin=" + leftMargin + ", pageWidth=" + pageWidth); throw new SkipDrawingException(); } xProps.setPropertyValue("HoriOrientPosition", posX + xOffsetToAdd); } break; case CENTER: xProps.setPropertyValue("HoriOrientRelation", RelOrientation.PAGE_FRAME); // 7:EntirePage, // 8:PageTextArea if (shape.horzOffset == 0) { xProps.setPropertyValue("HoriOrient", HoriOrientation.CENTER); // 1:Top, 2:Bottom, // 2:Center, 0:NONE(From // top) } else { xProps.setPropertyValue("HoriOrient", HoriOrientation.NONE); // 0:NONE=From top // 중간지점에서 frame 중심까지의 offset -> page상단부터의 frame상단까지 offset으로 계산 posX = (Transform.translateHwp2Office(page.width) - Transform.translateHwp2Office(shape.width)) / 2 + Transform.translateHwp2Office(shape.horzOffset); int leftMargin = Transform.translateHwp2Office(page.marginLeft); int shapeWidth = Transform.translateHwp2Office(shape.width); int pageWidth = Transform .translateHwp2Office(page.landscape ? page.height : page.width); // 보이는 부분이 50% 넘으면 보이도록 수정 if ((posX + xOffsetToAdd < 0 && (posX + xOffsetToAdd + shapeWidth / 2) < 0) || (posX + xOffsetToAdd + shapeWidth > pageWidth && (posX + xOffsetToAdd + shapeWidth / 2) > pageWidth)) { log.fine("posX=" + posX + ", shapeWidth=" + shapeWidth + ", pageLeftMargin=" + leftMargin + ", pageWidth=" + pageWidth); throw new SkipDrawingException(); } xProps.setPropertyValue("HoriOrientPosition", posX + xOffsetToAdd); } break; case RIGHT: // RIGHT case OUTSIDE: xProps.setPropertyValue("HoriOrientRelation", RelOrientation.PAGE_FRAME); // 7:EntirePage, // 8:PageTextArea if (shape.horzOffset == 0) { xProps.setPropertyValue("HoriOrient", HoriOrientation.RIGHT); // 1:Top, 2:Bottom, // 2:Center, 0:NONE(From // top) } else { xProps.setPropertyValue("HoriOrient", HoriOrientation.NONE); // 0:NONE=From top // page하단에서 frame 하단까지의 offset을 -> page상단부터의 frame상단까지 offset으로 계산 posX = Transform.translateHwp2Office(page.width) - Transform.translateHwp2Office(shape.width) - Transform.translateHwp2Office(shape.horzOffset); int leftMargin = Transform.translateHwp2Office(page.marginLeft); int shapeWidth = Transform.translateHwp2Office(shape.width); int pageWidth = Transform .translateHwp2Office(page.landscape ? page.height : page.width); // 보이는 부분이 50% 넘으면 보이도록 수정 if ((posX + xOffsetToAdd < 0 && (posX + xOffsetToAdd + shapeWidth / 2) < 0) || (posX + xOffsetToAdd + shapeWidth > pageWidth && (posX + xOffsetToAdd + shapeWidth / 2) > pageWidth)) { log.fine("posX=" + posX + ", shapeWidth=" + shapeWidth + ", pageLeftMargin=" + leftMargin + ", pageWidth=" + pageWidth); throw new SkipDrawingException(); } xProps.setPropertyValue("HoriOrientPosition", posX + xOffsetToAdd); } break; } break; case PAGE: switch (shape.horzAlign) { case LEFT: // LEFT case INSIDE: xProps.setPropertyValue("HoriOrientRelation", RelOrientation.PAGE_PRINT_AREA); // 7:EntirePage, // 8:PageTextArea if (shape.horzOffset == 0) { xProps.setPropertyValue("HoriOrient", HoriOrientation.LEFT); // 1:Top, 2:Bottom, // 2:Center, 0:NONE(From // top) } else { xProps.setPropertyValue("HoriOrient", HoriOrientation.NONE); // 0:NONE=From left // page상단으로부터 frame상단까지의 offset posX = Transform.translateHwp2Office(shape.horzOffset); int leftMargin = Transform.translateHwp2Office(page.marginLeft); int shapeWidth = Transform.translateHwp2Office(shape.width); int pageWidth = Transform .translateHwp2Office(page.landscape ? page.height : page.width); // 보이는 부분이 50% 넘으면 보이도록 수정 if ((posX + xOffsetToAdd < 0 && (posX + xOffsetToAdd + shapeWidth / 2) < 0) || (posX + xOffsetToAdd + shapeWidth > pageWidth && (posX + xOffsetToAdd + shapeWidth / 2) > pageWidth)) { log.fine("posX=" + posX + ", shapeWidth=" + shapeWidth + ", pageLeftMargin=" + leftMargin + ", pageWidth=" + pageWidth); throw new SkipDrawingException(); } xProps.setPropertyValue("HoriOrientPosition", posX + xOffsetToAdd); } break; case CENTER: xProps.setPropertyValue("HoriOrientRelation", RelOrientation.PAGE_PRINT_AREA); // 7:EntirePage, // 8:PageTextArea if (shape.horzOffset == 0) { xProps.setPropertyValue("HoriOrient", HoriOrientation.CENTER); // 1:Top, 2:Bottom, // 2:Center, 0:NONE(From // top) } else { xProps.setPropertyValue("HoriOrient", HoriOrientation.NONE); // 0:NONE=From left // 중간지점에서 frame 중심까지의 offset -> page상단부터의 frame상단까지 offset으로 계산 int pageWidth = Transform.translateHwp2Office(page.landscape ? page.height : page.width) - Transform.translateHwp2Office(page.marginLeft) - Transform.translateHwp2Office(page.marginRight); posX = (pageWidth - Transform.translateHwp2Office(shape.width)) / 2; posX += Transform.translateHwp2Office(shape.horzOffset); int leftMargin = Transform.translateHwp2Office(page.marginLeft); int shapeWidth = Transform.translateHwp2Office(shape.width); // 보이는 부분이 50% 넘으면 보이도록 수정 if ((posX + xOffsetToAdd < 0 && (posX + xOffsetToAdd + shapeWidth / 2) < 0) || (posX + xOffsetToAdd + shapeWidth > pageWidth && (posX + xOffsetToAdd + shapeWidth / 2) > pageWidth)) { log.fine("posX=" + posX + ", shapeWidth=" + shapeWidth + ", pageLeftMargin=" + leftMargin + ", pageWidth=" + pageWidth); throw new SkipDrawingException(); } xProps.setPropertyValue("HoriOrientPosition", posX + xOffsetToAdd); } break; case RIGHT: // RIGHT case OUTSIDE: xProps.setPropertyValue("HoriOrientRelation", RelOrientation.PAGE_PRINT_AREA); // 7:EntirePage, // 8:PageTextArea if (shape.horzOffset == 0) { xProps.setPropertyValue("HoriOrient", HoriOrientation.RIGHT); // 1:Top, 2:Bottom, // 2:Center, 0:NONE(From // top) } else { xProps.setPropertyValue("HoriOrient", HoriOrientation.NONE); // 0:NONE=From left // page하단에서 frame 하단까지의 offset을 -> page상단부터의 frame상단까지 offset으로 계산 int pageWidth = Transform.translateHwp2Office(page.landscape ? page.height : page.width) - Transform.translateHwp2Office(page.marginLeft) - Transform.translateHwp2Office(page.marginRight); posX = pageWidth - Transform.translateHwp2Office(shape.width) - Transform.translateHwp2Office(shape.horzOffset); int leftMargin = Transform.translateHwp2Office(page.marginLeft); int shapeWidth = Transform.translateHwp2Office(shape.width); // 보이는 부분이 50% 넘으면 보이도록 수정 if ((posX + xOffsetToAdd < 0 && (posX + xOffsetToAdd + shapeWidth / 2) < 0) || (posX + xOffsetToAdd + shapeWidth > pageWidth && (posX + xOffsetToAdd + shapeWidth / 2) > pageWidth)) { log.fine("posX=" + posX + ", shapeWidth=" + shapeWidth + ", pageLeftMargin=" + leftMargin + ", pageWidth=" + pageWidth); throw new SkipDrawingException(); } xProps.setPropertyValue("HoriOrientPosition", posX + xOffsetToAdd); } break; } break; case COLUMN: case PARA: switch (shape.horzAlign) { case LEFT: // LEFT case INSIDE: xProps.setPropertyValue("HoriOrientRelation", RelOrientation.PRINT_AREA); // 1:paragraph // text area if (shape.horzOffset == 0) { xProps.setPropertyValue("HoriOrient", HoriOrientation.LEFT); // 1:Top, 2:Bottom, // 2:Center, 0:NONE(From // top) } else { xProps.setPropertyValue("HoriOrient", HoriOrientation.NONE); // 0:NONE=From left // page상단으로부터 frame상단까지의 offset posX = Transform.translateHwp2Office(shape.horzOffset); int leftMargin = Transform.translateHwp2Office(page.marginLeft); int shapeWidth = Transform.translateHwp2Office(shape.width); int pageWidth = Transform .translateHwp2Office(page.landscape ? page.height : page.width); // 보이는 부분이 50% 넘으면 보이도록 수정 if ((posX + xOffsetToAdd + leftMargin < 0 && (posX + xOffsetToAdd + leftMargin + shapeWidth / 2) < 0) || (posX + xOffsetToAdd + shapeWidth > pageWidth && (posX + xOffsetToAdd + shapeWidth / 2) > pageWidth)) { log.fine("posX=" + posX + ", shapeWidth=" + shapeWidth + ", pageLeftMargin=" + leftMargin + ", pageWidth=" + pageWidth); throw new SkipDrawingException(); } xProps.setPropertyValue("HoriOrientPosition", posX + xOffsetToAdd); } break; case CENTER: xProps.setPropertyValue("HoriOrientRelation", RelOrientation.PRINT_AREA); // 7:EntirePage, // 8:PageTextArea if (shape.horzOffset == 0) { xProps.setPropertyValue("HoriOrient", HoriOrientation.CENTER); // 1:Top, 2:Bottom, // 2:Center, 0:NONE(From // top) } else { xProps.setPropertyValue("HoriOrient", HoriOrientation.NONE); // 0:NONE=From left // 중간지점에서 frame 중심까지의 offset -> page상단부터의 frame상단까지 offset으로 계산 int pageWidth = Transform.translateHwp2Office(page.landscape ? page.height : page.width) - Transform.translateHwp2Office(page.marginLeft) - Transform.translateHwp2Office(page.marginRight); posX = (pageWidth - Transform.translateHwp2Office(shape.width)) / 2; posX += Transform.translateHwp2Office(shape.horzOffset); int leftMargin = Transform.translateHwp2Office(page.marginLeft); int shapeWidth = Transform.translateHwp2Office(shape.width); // 보이는 부분이 50% 넘으면 보이도록 수정 if ((posX + xOffsetToAdd < 0 && (posX + xOffsetToAdd + shapeWidth / 2) < 0) || (posX + xOffsetToAdd + shapeWidth > pageWidth && (posX + xOffsetToAdd + shapeWidth / 2) > pageWidth)) { log.fine("posX=" + posX + ", shapeWidth=" + shapeWidth + ", pageLeftMargin=" + leftMargin + ", pageWidth=" + pageWidth); throw new SkipDrawingException(); } xProps.setPropertyValue("HoriOrientPosition", posX + xOffsetToAdd); } break; case RIGHT: // RIGHT case OUTSIDE: xProps.setPropertyValue("HoriOrientRelation", RelOrientation.PRINT_AREA); // 7:EntirePage, // 8:PageTextArea if (shape.horzOffset == 0) { xProps.setPropertyValue("HoriOrient", HoriOrientation.RIGHT); // 1:Top, 2:Bottom, // 2:Center, 0:NONE(From // top) } else { xProps.setPropertyValue("HoriOrient", HoriOrientation.NONE); // 0:NONE=From left // page하단에서 frame 하단까지의 offset을 -> page상단부터의 frame상단까지 offset으로 계산 int pageWidth = Transform.translateHwp2Office(page.landscape ? page.height : page.width) - Transform.translateHwp2Office(page.marginLeft) - Transform.translateHwp2Office(page.marginRight); posX = pageWidth - Transform.translateHwp2Office(shape.width) - Transform.translateHwp2Office(shape.horzOffset); int leftMargin = Transform.translateHwp2Office(page.marginLeft); int shapeWidth = Transform.translateHwp2Office(shape.width); // 보이는 부분이 50% 넘으면 보이도록 수정 if ((posX + xOffsetToAdd < 0 && (posX + xOffsetToAdd + shapeWidth / 2) < 0) || (posX + xOffsetToAdd + shapeWidth > pageWidth && (posX + xOffsetToAdd + shapeWidth / 2) > pageWidth)) { log.fine("posX=" + posX + ", shapeWidth=" + shapeWidth + ", pageLeftMargin=" + leftMargin + ", pageWidth=" + pageWidth); throw new SkipDrawingException(); } xProps.setPropertyValue("HoriOrientPosition", posX + xOffsetToAdd); } break; } break; } } } } catch (IllegalArgumentException | UnknownPropertyException | PropertyVetoException | WrappedTargetException e) { e.printStackTrace(); } } private static void setWrapStyle(XPropertySet xPropSet, Ctrl_GeneralShape shape) { // wrapStyle; // 0:어울림, 1:자리차지, 2:글 뒤로, 3:글 앞으로 // wrapText; // 0:양쪽, 1:왼쪽, 2:오른쪽, 3:큰쪽 try { if (shape.nGrp > 0) { xPropSet.setPropertyValue("Opaque", true); xPropSet.setPropertyValue("AllowOverlap", true); // THROUGH에서는 효과 없음. try { xPropSet.setPropertyValue("SurroundContour", false);// contour는 THROUGH에서는 효과 없음 } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("SurroundContour has exception"); } try { xPropSet.setPropertyValue("ContourOutside", false); // Frames 에서는 불가 } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("ContourOutside has exception"); } try { xPropSet.setPropertyValue("IsAutomaticContour", false); } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("IsAutomaticContour has exception"); } xPropSet.setPropertyValue("TextWrap", WrapTextMode.NONE); } else { switch (shape.textWrap) { case SQUARE: // 어울림 xPropSet.setPropertyValue("Opaque", true); xPropSet.setPropertyValue("AllowOverlap", true); // THROUGH에서는 효과 없음. WrapTextMode wrapText = WrapTextMode.NONE; boolean isAutomaticContour = false; switch (shape.textFlow) { case 0x0: // 양쪽 wrapText = WrapTextMode.PARALLEL; break; case 0x1: // 왼쪽 wrapText = WrapTextMode.LEFT; break; case 0x2: // 오른쪽 wrapText = WrapTextMode.RIGHT; break; case 0x3: // 큰쪽 wrapText = WrapTextMode.DYNAMIC; isAutomaticContour = true; break; } if (shape.treatAsChar == false) { try { xPropSet.setPropertyValue("SurroundContour", false);// contour는 THROUGH에서는 효과 없음 } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("SurroundContour has exception"); } try { xPropSet.setPropertyValue("ContourOutside", false); // Frames 에서는 불가 } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("ContourOutside has exception"); } try { xPropSet.setPropertyValue("IsAutomaticContour", isAutomaticContour); } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("IsAutomaticContour has exception"); } } xPropSet.setPropertyValue("TextWrap", wrapText); break; case TOP_AND_BOTTOM: // 자리차지 xPropSet.setPropertyValue("Opaque", true); if (shape.treatAsChar == false) { xPropSet.setPropertyValue("AllowOverlap", true); // THROUGH에서는 효과 없음. try { xPropSet.setPropertyValue("SurroundContour", false);// contour는 THROUGH에서는 효과 없음 } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("SurroundContour has exception"); } try { xPropSet.setPropertyValue("ContourOutside", false); // Frames 에서는 불가 } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("ContourOutside has exception"); } try { xPropSet.setPropertyValue("IsAutomaticContour", false); } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("IsAutomaticContour has exception"); } } xPropSet.setPropertyValue("TextWrap", WrapTextMode.NONE); break; case BEHIND_TEXT: // 글 뒤로 xPropSet.setPropertyValue("Opaque", false); if (shape.treatAsChar == false) { xPropSet.setPropertyValue("AllowOverlap", true); // THROUGH에서는 효과 없음. if (!(shape instanceof Ctrl_ShapeRect) && !(shape instanceof Ctrl_Container)) { try { xPropSet.setPropertyValue("SurroundContour", false);// contour는 THROUGH에서는 효과 없음 } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("SurroundContour has exception"); } try { xPropSet.setPropertyValue("ContourOutside", false); // Frames 에서는 불가 } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("ContourOutside has exception"); } try { xPropSet.setPropertyValue("IsAutomaticContour", false); } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("IsAutomaticContour has exception"); } } } xPropSet.setPropertyValue("TextWrap", WrapTextMode.THROUGH); break; case IN_FRONT_OF_TEXT: // 글 앞으로 xPropSet.setPropertyValue("Opaque", true); if (shape.treatAsChar == false) { xPropSet.setPropertyValue("AllowOverlap", true); // THROUGH에서는 효과 없음. if (!(shape instanceof Ctrl_ShapeRect) && !(shape instanceof Ctrl_Container)) { try { xPropSet.setPropertyValue("SurroundContour", false);// contour는 THROUGH에서는 효과 없음 } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("SurroundContour has exception"); } try { xPropSet.setPropertyValue("ContourOutside", false); // Frames 에서는 불가 } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("ContourOutside has exception"); } try { xPropSet.setPropertyValue("IsAutomaticContour", false); } catch (UnknownPropertyException | PropertyVetoException | IllegalArgumentException | WrappedTargetException e) { log.severe("IsAutomaticContour has exception"); } } } if (shape instanceof Ctrl_ShapeRect || shape instanceof Ctrl_ShapePolygon) { xPropSet.setPropertyValue("TextWrap", WrapTextMode.DYNAMIC); // Optimal } else { xPropSet.setPropertyValue("TextWrap", WrapTextMode.THROUGH); } break; } // 한컴에 있는 ZOrder(317,318)를 set하고 나서 odf를 열었을때 ZOrder(1,0)가 반전되는 현상 // 이 현상 때문에 그림위에 있는 TextFrame이 보이지 않는다. ZOrder 없이 화면에 뿌리는 순서대로 유지하도록 한다. // xPropSet.setPropertyValue("ZOrder", shape.zOrder); } } catch (IllegalArgumentException | UnknownPropertyException | PropertyVetoException | WrappedTargetException e) { e.printStackTrace(); } } private static void setLineStyle(XPropertySet xPropSet, Ctrl_GeneralShape shape) { try { if (shape.lineStyle == null) { xPropSet.setPropertyValue("LineStyle", com.sun.star.drawing.LineStyle.NONE); return; } else { switch (shape.lineStyle) { case NONE: xPropSet.setPropertyValue("LineStyle", com.sun.star.drawing.LineStyle.NONE); break; case SOLID: xPropSet.setPropertyValue("LineStyle", com.sun.star.drawing.LineStyle.SOLID); break; case DASH: xPropSet.setPropertyValue("LineStyle", com.sun.star.drawing.LineStyle.DASH); xPropSet.setPropertyValue("LineDashName", "Long Dash"); break; case DOT: xPropSet.setPropertyValue("LineStyle", com.sun.star.drawing.LineStyle.DASH); xPropSet.setPropertyValue("LineDashName", "Dot"); break; case DASH_DOT: xPropSet.setPropertyValue("LineStyle", com.sun.star.drawing.LineStyle.DASH); xPropSet.setPropertyValue("LineDashName", "Dash Dot"); break; case DASH_DOT_DOT: xPropSet.setPropertyValue("LineStyle", com.sun.star.drawing.LineStyle.DASH); xPropSet.setPropertyValue("LineDashName", "Dash Dot Dot"); break; case LONG_DASH: xPropSet.setPropertyValue("LineStyle", com.sun.star.drawing.LineStyle.DASH); xPropSet.setPropertyValue("LineDashName", "Long Dash"); break; case CIRCLE: xPropSet.setPropertyValue("LineStyle", com.sun.star.drawing.LineStyle.DASH); xPropSet.setPropertyValue("LineDashName", "Dot"); break; case DOUBLE_SLIM: xPropSet.setPropertyValue("LineStyle", com.sun.star.drawing.LineStyle.DASH); xPropSet.setPropertyValue("LineDashName", "Dot"); break; case SLIM_THICK: xPropSet.setPropertyValue("LineStyle", com.sun.star.drawing.LineStyle.DASH); xPropSet.setPropertyValue("LineDashName", "Dot"); break; case THICK_SLIM: xPropSet.setPropertyValue("LineStyle", com.sun.star.drawing.LineStyle.DASH); xPropSet.setPropertyValue("LineDashName", "Dot"); break; case SLIM_THICK_SLIM: xPropSet.setPropertyValue("LineStyle", com.sun.star.drawing.LineStyle.DASH); xPropSet.setPropertyValue("LineDashName", "Dot"); break; } // xPropSet.setPropertyValue("LineDash", lineDash); if (!(shape instanceof Ctrl_ShapePic)) { // Picture not support LineColor xPropSet.setPropertyValue("LineColor", shape.lineColor); } int convertedLineWidth = Transform.translateHwp2Office(shape.lineThick); log.finest("Line width=" + convertedLineWidth + " from " + shape.lineThick + " in HWP."); // TextFrame // Line 566(2mm) // Curve 33(0.12mm) // Polygon xPropSet.setPropertyValue("LineWidth", convertedLineWidth); if ((shape.outline & 0x1) == 0x1) { xPropSet.setPropertyValue("LineEndType", LineEndType.NONE); xPropSet.setPropertyValue("LineColor", shape.lineColor); } } } catch (IllegalArgumentException | UnknownPropertyException | PropertyVetoException | WrappedTargetException e) { e.printStackTrace(); } } private static void setArrowStyle(XPropertySet xPropSet, LineArrowStyle arrowStyle, LineArrowSize arrowWidth, boolean head) { String arrowStyleName = null; // UI에서 보이는 StyleName을 그대로 쓰면 안된다. IllegalArgumentException 발생. 검증된 영문기준 // ArrowStyleName만 쓰도록 한다. switch (arrowStyle) { case NORMAL: // 모양없음 arrowStyleName = null; break; case ARROW: // 화살모양 if (WriterContext.version >= 70) { arrowStyleName = arrowWidth.ordinal() < 3 ? "Arrow short" : arrowWidth.ordinal() < 6 ? "Arrow" : "Arrow large"; } else { arrowStyleName = "Arrow"; } break; case SPEAR: // 라인모양 if (WriterContext.version >= 70) { arrowStyleName = arrowWidth.ordinal() < 3 ? "Line short" : "Line"; } else { arrowStyleName = "Arrow"; } break; case CONCAVE_ARROW: // 오목한 화살모양 if (WriterContext.version >= 70) { arrowStyleName = arrowWidth.ordinal() < 3 ? "Concave short" : "Concave"; } else { arrowStyleName = "Arrow concave"; } break; case DIAMOND: // 속이 찬 다이아몬드 모양 arrowStyleName = "Diamond"; break; case CIRCLE: // 속이 찬 원 모양 arrowStyleName = "Circle"; break; case BOX: // 속이 찬 사각모양 arrowStyleName = "Square"; break; case EMPTY_DIAMOND: // 속이 빈 다이아몬드 모양 arrowStyleName = "Diamond unfilled"; break; case EMPTY_CIRCLE: // 속이 빈 원 모양 arrowStyleName = "Circle unfilled"; break; case EMPTY_BOX: // 속이 빈 사각모양 arrowStyleName = "Square unfilled"; break; } long arrowWidthNum = 0; switch (arrowWidth) { case SMALL_SMALL: // 작은-작은 case MEDIUM_SMALL: // 중간-작은 case LARGE_SMALL: // 큰-작은 arrowWidthNum = 203; break; case SMALL_MEDIUM: // 작은-중간 case MEDIUM_MEDIUM: // 중간-중간 case LARGE_MEDIUM: // 큰-중간 arrowWidthNum = 353; break; case SMALL_LARGE: // 작은-큰 case MEDIUM_LARGE: // 중간-큰 case LARGE_LARGE: // 큰-큰 arrowWidthNum = 499; } try { /* * // 크기를 설정하면 화살표가 나타나지 않는다. 어떤값을 주던 크기가 0으로 설정된다. 막아 놓는다. if (arrowWidthNum>0) * { // xPropSet.setPropertyValue(start?"LineStartWidth":"LineEndWidth", * arrowWidthNum); } */ if (arrowStyleName != null && !arrowStyleName.isEmpty()) { xPropSet.setPropertyValue(head ? "LineStartName" : "LineEndName", arrowStyleName); } xPropSet.setPropertyValue(head ? "LineStartCenter" : "LineEndCenter", false); } catch (Exception e) { log.severe(e.getMessage()); e.printStackTrace(); } } private static void setFillStyle(WriterContext wContext, XPropertySet xPropSet, Fill fill) throws Exception { try { if (fill == null) { xPropSet.setPropertyValue("FillStyle", com.sun.star.drawing.FillStyle.NONE); // Fill 이 없는 경우는 Transparency 100%로 설정한다. xPropSet.setPropertyValue("FillTransparence", 100); } else { if (fill.isColorFill()) { xPropSet.setPropertyValue("FillColor", fill.faceColor); com.sun.star.drawing.Hatch hatch = new com.sun.star.drawing.Hatch(); switch (fill.hatchStyle) { case NONE: xPropSet.setPropertyValue("FillStyle", com.sun.star.drawing.FillStyle.SOLID); break; case VERTICAL: // - - - xPropSet.setPropertyValue("FillStyle", com.sun.star.drawing.FillStyle.HATCH); hatch.Style = com.sun.star.drawing.HatchStyle.SINGLE; hatch.Color = fill.hatchColor; hatch.Distance = 100; hatch.Angle = 0; xPropSet.setPropertyValue("FillHatch", hatch); xPropSet.setPropertyValue("FillBackground", true); break; case HORIZONTAL: // ||||| xPropSet.setPropertyValue("FillStyle", com.sun.star.drawing.FillStyle.HATCH); hatch.Style = com.sun.star.drawing.HatchStyle.SINGLE; hatch.Color = fill.hatchColor; hatch.Distance = 100; hatch.Angle = 900; xPropSet.setPropertyValue("FillHatch", hatch); xPropSet.setPropertyValue("FillBackground", true); break; case BACK_SLASH: // \\\\\ xPropSet.setPropertyValue("FillStyle", com.sun.star.drawing.FillStyle.HATCH); hatch.Style = com.sun.star.drawing.HatchStyle.SINGLE; hatch.Color = fill.hatchColor; hatch.Distance = 100; hatch.Angle = 1350; xPropSet.setPropertyValue("FillHatch", hatch); xPropSet.setPropertyValue("FillBackground", true); break; case SLASH: // ///// xPropSet.setPropertyValue("FillStyle", com.sun.star.drawing.FillStyle.HATCH); hatch.Style = com.sun.star.drawing.HatchStyle.SINGLE; hatch.Color = fill.hatchColor; hatch.Distance = 100; hatch.Angle = 450; xPropSet.setPropertyValue("FillHatch", hatch); xPropSet.setPropertyValue("FillBackground", true); break; case CROSS: // +++++ xPropSet.setPropertyValue("FillStyle", com.sun.star.drawing.FillStyle.HATCH); hatch.Style = com.sun.star.drawing.HatchStyle.DOUBLE; hatch.Color = fill.hatchColor; hatch.Distance = 100; hatch.Angle = 0; xPropSet.setPropertyValue("FillHatch", hatch); xPropSet.setPropertyValue("FillBackground", true); break; case CROSS_DIAGONAL: // xxxxx xPropSet.setPropertyValue("FillStyle", com.sun.star.drawing.FillStyle.HATCH); hatch.Style = com.sun.star.drawing.HatchStyle.DOUBLE; hatch.Color = fill.hatchColor; hatch.Distance = 100; hatch.Angle = 450; xPropSet.setPropertyValue("FillHatch", hatch); xPropSet.setPropertyValue("FillBackground", true); break; } } if (fill.isGradFill()) { xPropSet.setPropertyValue("FillColor", fill.faceColor); xPropSet.setPropertyValue("FillStyle", com.sun.star.drawing.FillStyle.GRADIENT); com.sun.star.awt.Gradient gradient = new com.sun.star.awt.Gradient(); gradient.StartColor = fill.colors[0]; gradient.EndColor = fill.colors[1]; gradient.StartIntensity = (short)100; gradient.EndIntensity = (short)100; gradient.Angle = (short) (fill.angle * 10); // 1/10 degree로 맞춘다. gradient.XOffset = (short) Transform.translateHwp2Office(fill.centerX); gradient.YOffset = (short) Transform.translateHwp2Office(fill.centerY); gradient.StepCount = (short) fill.step; switch (fill.gradType) { case LINEAR: // 줄무니형 gradient.Style = com.sun.star.awt.GradientStyle.LINEAR; break; case RADIAL: // 원형 gradient.Style = com.sun.star.awt.GradientStyle.RADIAL; break; case CONICAL: // 원뿔형 gradient.Style = com.sun.star.awt.GradientStyle.AXIAL; break; case SQUARE: // 사각형 gradient.Style = com.sun.star.awt.GradientStyle.RECT; break; } xPropSet.setPropertyValue("FillGradient", gradient); } if (fill.isImageFill()) { fillGraphic(wContext, xPropSet, fill); } if (fill.isColorFill() == false && fill.isImageFill() == false && fill.isGradFill() == false) { xPropSet.setPropertyValue("FillStyle", com.sun.star.drawing.FillStyle.NONE); // Fill 이 없는 경우는 Transparency 100%로 설정한다. xPropSet.setPropertyValue("FillTransparence", 100); } } } catch (IllegalArgumentException | UnknownPropertyException | PropertyVetoException | WrappedTargetException e) { e.printStackTrace(); } // Open Bezier 이므로 Fill 하지 않는다. } public static void transform(XPropertySet xPropsSet, Ctrl_GeneralShape shape) throws UnknownPropertyException, WrappedTargetException, IllegalArgumentException, PropertyVetoException { HomogenMatrix3 aHomogenMatrix3 = getTransformedMatrix(shape); xPropsSet.setPropertyValue("Transformation", aHomogenMatrix3); } public static HomogenMatrix3 getTransformedMatrix(Ctrl_GeneralShape shape) throws UnknownPropertyException, WrappedTargetException, IllegalArgumentException, PropertyVetoException { // transform 방식 변경 (2024.01.28) // 화면에 크기(0,0)로 그린 후, getProperty하지 않고, transformation값 연산, 이후 setProperty로 설정 // HomogenMatrix3 aHomogenMatrix3 = (HomogenMatrix3) xPropsSet.getPropertyValue("Transformation"); HomogenMatrix3 aHomogenMatrix3 = new HomogenMatrix3(); aHomogenMatrix3.Line1.Column1 = shape.iniWidth; aHomogenMatrix3.Line2.Column2 = shape.iniHeight; aHomogenMatrix3.Line3.Column3 = 1; AffineTransform prevMatrix = new AffineTransform(aHomogenMatrix3.Line1.Column1, aHomogenMatrix3.Line2.Column1, aHomogenMatrix3.Line1.Column2, aHomogenMatrix3.Line2.Column2, aHomogenMatrix3.Line1.Column3, aHomogenMatrix3.Line2.Column3); for (int i = shape.matCnt - 1; i >= 0; i--) { // 1. scale matrix AffineTransform scaleMatrix = new AffineTransform(shape.matrixSeq[i * 12 + 0], shape.matrixSeq[i * 12 + 3], shape.matrixSeq[i * 12 + 1], shape.matrixSeq[i * 12 + 4], shape.matrixSeq[i * 12 + 2], shape.matrixSeq[i * 12 + 5]); scaleMatrix.concatenate(prevMatrix); // 2. rotation matrix AffineTransform rotatMatrix = new AffineTransform(shape.matrixSeq[i * 12 + 6], shape.matrixSeq[i * 12 + 9], shape.matrixSeq[i * 12 + 7], shape.matrixSeq[i * 12 + 10], shape.matrixSeq[i * 12 + 8], shape.matrixSeq[i * 12 + 11]); rotatMatrix.concatenate(scaleMatrix); prevMatrix = rotatMatrix; } // 3. translation matrix AffineTransform translateMatrix = new AffineTransform(shape.matrix[0], shape.matrix[3], shape.matrix[1], shape.matrix[4], shape.matrix[2], shape.matrix[5]); translateMatrix.concatenate(prevMatrix); // 4. Hwp Unit -> LO Unit AffineTransform hwp2LoScale = new AffineTransform((double) 21000 / 59529, 0, 0, (double) 21000 / 59529, 0, 0); hwp2LoScale.concatenate(translateMatrix); double transformMatrix[] = new double[6]; hwp2LoScale.getMatrix(transformMatrix); // convert the flatMatrix to our HomogenMatrix3 structure aHomogenMatrix3.Line1.Column1 = transformMatrix[0]; aHomogenMatrix3.Line2.Column1 = transformMatrix[1]; aHomogenMatrix3.Line1.Column2 = transformMatrix[2]; aHomogenMatrix3.Line2.Column2 = transformMatrix[3]; aHomogenMatrix3.Line1.Column3 = transformMatrix[4]; aHomogenMatrix3.Line2.Column3 = transformMatrix[5]; return aHomogenMatrix3; } public static void fillGraphic(WriterContext wContext, XPropertySet xPropSet, Fill fill) { try { Object graphicProviderObject = wContext.mMCF.createInstanceWithContext("com.sun.star.graphic.GraphicProvider", wContext.mContext); XGraphicProvider xGraphicProvider = UnoRuntime.queryInterface(XGraphicProvider.class, graphicProviderObject); byte[] imageAsByteArray = wContext.getBinBytes(fill.binItemID); if (imageAsByteArray != null) { PropertyValue[] v = new PropertyValue[2]; v[0] = new PropertyValue(); v[0].Name = "InputStream"; v[0].Value = new ByteArrayToXInputStreamAdapter(imageAsByteArray); v[1] = new PropertyValue(); v[1].Name = "MimeType"; String imageFormat = wContext.getBinFormat(fill.binItemID).toLowerCase(); switch (imageFormat) { case "png": v[1].Value = "image/png"; break; case "bmp": v[1].Value = "image/bmp"; break; case "wmf": v[1].Value = "image/x-wmf"; break; case "jpg": case "jpeg": v[1].Value = "image/jpeg"; break; case "gif": v[1].Value = "image/gif"; break; case "tif": v[1].Value = "image/tiff"; break; case "svg": v[1].Value = "image/svg+xml"; break; } XGraphic graphic = xGraphicProvider.queryGraphic(v); try { Path homeDir = wContext.userHomeDir; Path path = Files.createTempFile(homeDir, "H2O_IMG_", "_" + fill.binItemID + "." + imageFormat); URL url = path.toFile().toURI().toURL(); String urlString = url.toExternalForm(); v[0].Name = "URL"; v[0].Value = urlString; xGraphicProvider.storeGraphic(graphic, v); Object bt = wContext.mMSF.createInstance("com.sun.star.drawing.BitmapTable"); XNameContainer bitmapContainer = UnoRuntime.queryInterface(XNameContainer.class, bt); try { log.fine("FillBMP" + fill.binItemID + " saved to " + urlString); bitmapContainer.insertByName("FillBMP" + fill.binItemID, urlString); } catch (com.sun.star.container.ElementExistException e) { } XNameAccess bitmapAccess = (XNameAccess) UnoRuntime.queryInterface(XNameAccess.class, bt); Object ob = null; xPropSet.setPropertyValue("FillStyle", com.sun.star.drawing.FillStyle.BITMAP); /* * 여러개 FillBitmap 처리시 같은 이미지만 반복됨. 대신 FillBitmapName을 사용하도록 함 * try { * ob =bitmapAccess.getByName("FillBMP"+String.valueOf(fill.binItem)); * XBitmap xBitmap = (XBitmap)UnoRuntime.queryInterface(XBitmap.class, ob); * xPropSet.setPropertyValue("FillBitmap", xBitmap); * } catch (com.sun.star.container.NoSuchElementException e) { * } */ xPropSet.setPropertyValue("FillBitmapName", "FillBMP" + fill.binItemID); xPropSet.setPropertyValue("FillBitmapMode", BitmapMode.STRETCH); Files.delete(path); } catch (IOException | Exception e) { } } } catch (IllegalArgumentException | Exception e) { e.printStackTrace(); } } } H2Orestart-0.7.2/source/soffice/ConvNumbering.java000066400000000000000000000740311476273367000220760ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package soffice; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.imageio.ImageIO; import com.sun.star.awt.FontDescriptor; import com.sun.star.awt.FontSlant; import com.sun.star.awt.Size; import com.sun.star.beans.PropertyValue; import com.sun.star.beans.XPropertySet; import com.sun.star.container.XIndexReplace; import com.sun.star.container.XNameAccess; import com.sun.star.container.XNameContainer; import com.sun.star.style.NumberingType; import com.sun.star.style.XStyle; import com.sun.star.style.XStyleFamiliesSupplier; import com.sun.star.text.HoriOrientation; import com.sun.star.text.LabelFollow; import com.sun.star.text.PositionAndSpaceMode; import com.sun.star.text.VertOrientation; import com.sun.star.text.XParagraphCursor; import com.sun.star.uno.Exception; import com.sun.star.uno.UnoRuntime; import HwpDoc.HwpElement.HwpRecord_Bullet; import HwpDoc.HwpElement.HwpRecord_Numbering; import HwpDoc.HwpElement.HwpRecord_Numbering.Numbering; import HwpDoc.paragraph.Ctrl_SectionDef; public class ConvNumbering { private static final Logger log = Logger.getLogger(ConvNumbering.class.getName()); public static Map numberingStyleNameMap = new HashMap(); public static Map bulletStyleNameMap = new HashMap(); private static final String NUMBERING_STYLE_PREFIX = "HWP numbering "; private static final String BULLET_STYLE_PREFIX = "HWP bullet "; // UNOAPI를 사용하지 않는 경우, 현재 numbering 값을 가져오기 위해 사용 private static Map numberingNumbersMap = new HashMap(); private static Map prevNumberingLevelMap = new HashMap(); private static Map bulletNumbersMap = new HashMap(); public static void reset(WriterContext wContext) { deleteCustomStyles(wContext); } private static void deleteCustomStyles(WriterContext wContext) { if (wContext.mMyDocument!=null) { try { XStyleFamiliesSupplier xSupplier = (XStyleFamiliesSupplier)UnoRuntime.queryInterface(XStyleFamiliesSupplier.class, wContext.mMyDocument); XNameAccess xFamilies = (XNameAccess) UnoRuntime.queryInterface (XNameAccess.class, xSupplier.getStyleFamilies()); XNameContainer xNumeringFamily = (XNameContainer) UnoRuntime.queryInterface(XNameContainer.class, xFamilies.getByName("NumberingStyles")); for (Integer custIndex: numberingStyleNameMap.keySet()) { if (xNumeringFamily.hasByName(numberingStyleNameMap.get(custIndex))) { xNumeringFamily.removeByName(numberingStyleNameMap.get(custIndex)); } } for (Integer custIndex: bulletStyleNameMap.keySet()) { if (xNumeringFamily.hasByName(bulletStyleNameMap.get(custIndex))) { xNumeringFamily.removeByName(bulletStyleNameMap.get(custIndex)); } } } catch (Exception e) { e.printStackTrace(); } } numberingStyleNameMap.clear(); bulletStyleNameMap.clear(); } private static void setNumberingProp(PropertyValue[] aProps, int i, HwpRecord_Numbering numbering) { short numberingType = -1; short adjust = -1; short parentNumbering = -1; String prefix = ""; String suffix = ""; String charStyleName = ""; String listFormat = ""; short startsWith = -1; short positionAndSpaceMode = PositionAndSpaceMode.LABEL_ALIGNMENT; short labelFollowedBy = LabelFollow.SPACE; long listtabStopPosition = -1; long firstLineIndent = -1; long indentAt = -1; int charShapeId; // Hwp에서 수준(level)은 7개+확장3개, LibreOffice에서 level은 10개이다. if (i<7) { Numbering numb = numbering.numbering[i]; if (numb!=null) { adjust = numb.align==0x0?HoriOrientation.LEFT:numb.align==0x1?HoriOrientation.CENTER:numb.align==0x2?HoriOrientation.RIGHT:HoriOrientation.NONE; // boolean useInstWidth; // 문단머리정보 - 번호 너비를 실제 인스턴스 문자열의 너비에 따를지 여부 // boolean autoIndent; // 문단머리정보 - 자동 내어 쓰기 여부 // byte textOffsetType; // 문단머리정보 - 수준별 본문과의 거리 종류 // short widthAdjust; // 문단머리정보 - 너비 보정값 // short textOffset; // 문단머리정보 - 본문과의 거리 // int charShape; // 문단머리정보 - 글자 모양 아이디 참조 // int start; // String numFormat; // 번호 형식 parentNumbering = getParentNumbering(numb.numFormat, i); prefix = getPrefix(numb.numFormat); suffix = getSuffix(numb.numFormat); listFormat = getNumberFormat(numb.numFormat); numberingType = getNumberingType(numb.numFormat, i); charShapeId = numb.charShape+1; charStyleName = ConvPara.getCharStyleName(charShapeId); if (numb.textOffsetType==0x1) { // 절대값 거리 indentAt = Transform.translateHwp2Office(numb.textOffset); listtabStopPosition = indentAt/2; firstLineIndent = -listtabStopPosition; } else { // 상대값 거리 (글자의 몇%) indentAt = 1400; listtabStopPosition = indentAt/2; firstLineIndent = -listtabStopPosition; } startsWith = (short)numbering.numbering[i].startNumber; } } else { startsWith = (short)numbering.extLevelStart[i-7]; parentNumbering = getParentNumbering(numbering.extLevelFormat[i-7], i); prefix = getPrefix(numbering.extLevelFormat[i-7]); suffix = getSuffix(numbering.extLevelFormat[i-7]); numberingType = getNumberingType(numbering.extLevelFormat[i-7], i); } for (int j=0; j0?0:bullet.bulletImage==0?3:3); PropertyValue[] newProps = new PropertyValue[aProps.length+extendSize]; System.arraycopy(aProps, 0, newProps, 0, aProps.length); for (int j=0; j 0) { int nextIndex = format.lastIndexOf("^", lastIndex-1); if (nextIndex == -1) { prefix = format.substring(lastIndex-1, lastIndex); } } return prefix; } public static String getSuffix(String format) { String suffix = ""; if (format==null||format.equals("")) return suffix; int lastIndex = format.lastIndexOf("^"); if (lastIndex > -1) { if (lastIndex+2 < format.length()) { suffix = format.substring(lastIndex+2, lastIndex+3); } } else if (format.length()==1) { suffix = format; } return suffix; } public static String getNumberFormat(String format) { // "" // "^2." // "^2.^3." // "^2.^3.^4" String formatStr = ""; if (format==null||format.equals("")) return formatStr; Pattern pattern = Pattern.compile("\\^\\d+"); Matcher m = pattern.matcher(format); StringBuffer sb = new StringBuffer(); int prevEnd = 0; while(m.find()) { int s = m.start(); int e = m.end(); sb.append(format.subSequence(prevEnd, s)); int num = Integer.parseInt(format.substring(s+1, e)); sb.append("%" + num + "%"); prevEnd = e; } sb.append(format.substring(prevEnd)); return sb.toString(); } public static short getNumberingType(String format, int i) { // 1, 2, 3, 4 ARABIC // a, b, c, e, CHARS_LOWER_LETTER // A, B, C, D, CHARS_UPPER_LETTER // CIRCLE_NUMBER // ㉠,㉡,㉢ HANGUL_CIRCLED_JAMO_KO // ㉮,㉯,㉰ HANGUL_CIRCLED_SYLLABLE_KO // ㄱ,ㄴ,ㄷ HANGUL_JAMO_KO // 가,나,다 HANGUL_SYLLABLE_KO // 일,이,삼 NUMBER_HANGUL_KO // 一,二,三 NUMBER_LOWER_ZH // i, ii, iii, ROMAN_LOWER // I, II, III, ROMAN_UPPER if (format==null||format.equals("")) { return NumberingType.NUMBER_NONE; } int lastIndex = format.lastIndexOf("^"); if (lastIndex > -1) { if (lastIndex+1 < format.length()) { char num = format.charAt(lastIndex+1); if (num >= 0x30 && num <= 0x39) { return NumberingType.ARABIC; } else if (num >= 0x41 && num <= 0x5A) { return NumberingType.CHARS_UPPER_LETTER; } else if (num >= 0x61 && num <= 0x7A) { return NumberingType.CHARS_LOWER_LETTER; } else if (num >= 0x2160 && num <= 0x2169) { return NumberingType.ROMAN_UPPER; } else if (num >= 0x2170 && num <= 0x2179) { return NumberingType.ROMAN_LOWER; } else if (num >= 0x2460 && num <= 0x2473) { return NumberingType.CIRCLE_NUMBER; } else if (num >= 0x3131 && num <= 0x314E) { return NumberingType.HANGUL_JAMO_KO; } else if (num >= 0x3260 && num <= 0x326D) { return NumberingType.HANGUL_CIRCLED_JAMO_KO; } else if (num >= 0x326E && num <= 0x327B) { return NumberingType.HANGUL_CIRCLED_SYLLABLE_KO; } else if (num >= 0xAC00 && num <= 0xC773) { return NumberingType.HANGUL_SYLLABLE_KO; } else if (num >= 0xC774 && num <= 0xC0BC) { return NumberingType.NUMBER_HANGUL_KO; } else if (num >= 0x4E00 && num <= 0x9FCB) { return NumberingType.NUMBER_LOWER_ZH; } } } return NumberingType.NUMBER_NONE; } } H2Orestart-0.7.2/source/soffice/ConvPage.java000066400000000000000000001434761476273367000210360ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package soffice; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.logging.Logger; import com.sun.star.awt.FontDescriptor; import com.sun.star.awt.XDevice; import com.sun.star.awt.XToolkit; import com.sun.star.beans.PropertyValue; import com.sun.star.beans.PropertyVetoException; import com.sun.star.beans.UnknownPropertyException; import com.sun.star.beans.XPropertySet; import com.sun.star.container.NoSuchElementException; import com.sun.star.container.XNameAccess; import com.sun.star.container.XNameContainer; import com.sun.star.graphic.XGraphic; import com.sun.star.graphic.XGraphicProvider; import com.sun.star.lang.DisposedException; import com.sun.star.lang.IllegalArgumentException; import com.sun.star.lang.WrappedTargetException; import com.sun.star.lib.uno.adapter.ByteArrayToXInputStreamAdapter; import com.sun.star.style.BreakType; import com.sun.star.style.GraphicLocation; import com.sun.star.style.NumberingType; import com.sun.star.style.PageStyleLayout; import com.sun.star.style.VerticalAlignment; import com.sun.star.style.XStyle; import com.sun.star.style.XStyleFamiliesSupplier; import com.sun.star.text.ControlCharacter; import com.sun.star.text.PageNumberType; import com.sun.star.text.TextColumn; import com.sun.star.text.XText; import com.sun.star.text.XTextColumns; import com.sun.star.text.XTextCursor; import com.sun.star.text.XTextDocument; import com.sun.star.text.XTextField; import com.sun.star.uno.Exception; import com.sun.star.uno.UnoRuntime; import HwpDoc.HwpElement.HwpRecord_CharShape; import HwpDoc.HwpElement.HwpRecord_ParaShape; import HwpDoc.paragraph.Ctrl_AutoNumber; import HwpDoc.paragraph.Ctrl_ColumnDef; import HwpDoc.paragraph.Ctrl_HeadFoot; import HwpDoc.paragraph.Ctrl_SectionDef; import HwpDoc.paragraph.Ctrl_ShapePic; import HwpDoc.paragraph.HwpParagraph; import HwpDoc.section.Page; public class ConvPage { private static final Logger log = Logger.getLogger(ConvPage.class.getName()); private static Map pageStyleNameMap = new HashMap(); private static Map pageMap = new HashMap(); private static List headerDone = new ArrayList<>(); // Header 중복을 private static List footerDone = new ArrayList<>(); private static int customIndex = 0; private static int secdIndex = 0; private static final String PAGE_STYLE_PREFIX = "HWP "; public static int getSectionIndex() { return secdIndex; } public static void setSectionIndex(int index) { secdIndex = index; } public static Ctrl_SectionDef getCurrentPage() { return pageMap.get(secdIndex); } public static void reset(WriterContext wContext) { deleteAllCustomPageStyle(wContext); headerDone.clear(); footerDone.clear(); secdIndex = 0; customIndex = 0; } private static void deleteAllCustomPageStyle(WriterContext wContext) { if (wContext.mMyDocument != null) { try { XStyleFamiliesSupplier xSupplier = (XStyleFamiliesSupplier) UnoRuntime .queryInterface(XStyleFamiliesSupplier.class, wContext.mMyDocument); XNameAccess xFamilies = (XNameAccess) UnoRuntime.queryInterface(XNameAccess.class, xSupplier.getStyleFamilies()); XNameContainer xFamily = (XNameContainer) UnoRuntime.queryInterface(XNameContainer.class, xFamilies.getByName("PageStyles")); for (Integer custIndex : pageStyleNameMap.keySet()) { try { if (xFamily.hasByName(pageStyleNameMap.get(custIndex))) { xFamily.removeByName(pageStyleNameMap.get(custIndex)); } } catch (DisposedException e) { log.severe(e.getMessage()); } } } catch (NoSuchElementException | WrappedTargetException e) { log.severe(e.getMessage()); } } pageStyleNameMap.clear(); pageMap.clear(); } public static void adjustFontIfNotExists(WriterContext wContext) { try { Object o = wContext.mMCF.createInstanceWithContext("com.sun.star.awt.Toolkit", wContext.mContext); XToolkit xToolkit = UnoRuntime.queryInterface(XToolkit.class, o); XDevice device = xToolkit.createScreenCompatibleDevice(0, 0); FontDescriptor[] fds = device.getFontDescriptors(); for (int i = 0; i < fds.length; i++) { WriterContext.fontNameSet.add(fds[i].Name); } for (int i = 0; i < wContext.getDocInfo().charShapeList.size(); i++) { HwpRecord_CharShape font = (HwpRecord_CharShape) wContext.getDocInfo().charShapeList.get(i); if (font.fontName[0]!=null && WriterContext.fontNameSet.contains(font.fontName[0])==false) { String replaceFontName = null; // 한컴 전용폰트이므로, 대체폰트로 교체. switch (font.fontName[0]) { case "한컴 윤고딕 720": replaceFontName = "나눔스퀘어 Light"; break; case "한컴 윤고딕 740": replaceFontName = "나눔스퀘어"; break; case "한컴 윤체 M": case "한컴 윤고딕 230": case "한컴 윤고딕 240": case "한컴 윤고딕 760": replaceFontName = "나눔스퀘어 Bold"; break; case "한컴 윤체 B": case "한컴 윤고딕 250": case "HY견고딕": replaceFontName = "나눔스퀘어 ExtraBold"; break; case "가는안상수체": case "중간안상수체": case "굵은안상수체": case "안상수2006가는": case "안상수2006굵은": case "안상수2006중간": replaceFontName = "나눔바른펜"; break; case "HY궁서": replaceFontName = "궁서"; break; case "HY그래픽": case "휴먼고딕": replaceFontName = "나눔고딕코딩"; break; case "휴먼옛체": replaceFontName = "나눔명조 옛한글"; break; case "HY견명조": replaceFontName = "나눔명조 ExtraBold"; break; default: replaceFontName = "나눔고딕"; } for (int j = 0; j < font.fontName.length; j++) { if (WriterContext.fontNameSet.contains(replaceFontName)) { log.fine("Font[" + font.fontName[j] + "] does not exist. so replacing with basic Font[" + replaceFontName + "]"); font.fontName[j] = replaceFontName; } } } } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } // Hwp에 맞는 Page Style을 미리 등록해놓자. public static String makeCustomPageStyle(WriterContext wContext, Ctrl_SectionDef secd) { String styleName = null; try { XStyle xPageStyle = UnoRuntime.queryInterface(XStyle.class, wContext.mMSF.createInstance("com.sun.star.style.PageStyle")); XStyleFamiliesSupplier xSupplier = (XStyleFamiliesSupplier) UnoRuntime .queryInterface(XStyleFamiliesSupplier.class, wContext.mMyDocument); XNameAccess xFamilies = (XNameAccess) UnoRuntime.queryInterface(XNameAccess.class, xSupplier.getStyleFamilies()); XNameContainer xFamily = (XNameContainer) UnoRuntime.queryInterface(XNameContainer.class, xFamilies.getByName("PageStyles")); styleName = PAGE_STYLE_PREFIX + customIndex; if (xFamily.hasByName(styleName) == false) { xFamily.insertByName(styleName, xPageStyle); } XPropertySet xStyleProps = (XPropertySet) UnoRuntime.queryInterface(XPropertySet.class, xPageStyle); // DEBUG XPropertySet propSet = UnoRuntime.queryInterface(XPropertySet.class, wContext.mTextCursor); propSet.setPropertyValue("PageDescName", styleName); // DEBUG // Size,Width,Height,Hidden,TextVerticalAdjust, // LeftMargin,RightMargin,TopMargin,BottomMargin, if (secd.page.landscape == false) { xStyleProps.setPropertyValue("Width", Integer.valueOf(Transform.translateHwp2Office(secd.page.width))); xStyleProps.setPropertyValue("Height", Integer.valueOf(Transform.translateHwp2Office(secd.page.height))); } else { xStyleProps.setPropertyValue("Width", Integer.valueOf(Transform.translateHwp2Office(secd.page.height))); xStyleProps.setPropertyValue("Height", Integer.valueOf(Transform.translateHwp2Office(secd.page.width))); } xStyleProps.setPropertyValue("LeftMargin", Integer.valueOf(Transform.translateHwp2Office(secd.page.marginLeft))); xStyleProps.setPropertyValue("RightMargin", Integer.valueOf(Transform.translateHwp2Office(secd.page.marginRight))); // LeftBorder,RightBorder,TopBorder,BottomBorder,BorderDistance,LeftBorderDistance,RightBorderDistance,TopBorderDistance,BottomBorderDistance, // TextColumns, // UserDefinedAttributes, // StandardPageMode, // FirstIsShared, // IsLandscape,NumberingType,PageStyleLayout,PrinterPaperTray, // RegisterParagraphStyle, // FollowStyle,IsPhysical,DisplayName, // WritingMode, // ShadowFormat,ShadowTransparence, // RubyBelow,GridRubyHeight, // GridMode,GridLines,GridPrint,GridDisplay,GridBaseWidth,GridSnapToChars,GridBaseHeight,GridColor, // FillColor,FillStyle,FillColor2,FillBackground, // FillBitmapMode,FillBitmapName,FillBitmap,FillBitmapURL,FillBitmapSizeX,FillBitmapLogicalSize,FillBitmapOffsetX,FillBitmapOffsetY,FillBitmapPositionOffsetX, // FillBitmapPositionOffsetY,FillBitmapRectanglePoint,FillBitmapSizeY,FillBitmapTile,FillBitmapStretch, // FillGradientStepCount,FillGradient,FillGradientName,FillHatch,FillHatchName, // FillTransparence,FillTransparenceGradient,FillTransparenceGradientName, // FootnoteHeight,FootnoteLineAdjust,FootnoteLineColor,FootnoteLineTextDistance,FootnoteLineWeight,FootnoteLineStyle,FootnoteLineRelativeWidth,FootnoteLineDistance, // HeaderIsOn,HeaderText,HeaderTextFirst,HeaderTextLeft,HeaderTextRight,HeaderIsShared,HeaderIsDynamicHeight,HeaderHeight, // HeaderDynamicSpacing,HeaderBodyDistance, // HeaderLeftMargin,HeaderRightMargin, // HeaderLeftBorder,HeaderRightBorder,HeaderTopBorder,HeaderBottomBorder,HeaderBorderDistance, // HeaderLeftBorderDistance,HeaderRightBorderDistance,HeaderTopBorderDistance,HeaderBottomBorderDistance, // HeaderFillBackground,HeaderFillColor,HeaderFillColor2,HeaderFillHatchName,HeaderFillStyle, // HeaderFillBitmap,HeaderFillBitmapName,HeaderFillBitmapSizeY,HeaderFillBitmapPositionOffsetY,HeaderFillBitmapLogicalSize,HeaderFillBitmapStretch,HeaderFillBitmapPositionOffsetX, // HeaderFillTransparence,HeaderFillTransparenceGradient,HeaderFillTransparenceGradientName, // HeaderFillGradientStepCount,HeaderFillGradient,HeaderFillGradientName, // HeaderBackColor,HeaderBackGraphicURL,HeaderBackGraphic,HeaderBackGraphicFilter,HeaderBackTransparent,HeaderBackGraphicLocation, // HeaderShadowFormat, // HeaderFillBitmapTile,HeaderFillBitmapMode,HeaderFillBitmapOffsetX,HeaderFillBitmapOffsetY,HeaderFillBitmapRectanglePoint,HeaderFillBitmapSizeX, // HeaderFillHatch, if (secd.hideHeader == false) { if (secd.page.marginHeader >= 0) { xStyleProps.setPropertyValue("TopMargin", Integer.valueOf(Transform.translateHwp2Office(secd.page.marginTop))); xStyleProps.setPropertyValue("HeaderIsOn", true); // 한컴(marginHeader) = // LO(HeaderHeight+HeaderBodyDistance) xStyleProps.setPropertyValue("HeaderHeight", Integer.valueOf(Transform.translateHwp2Office(secd.page.marginHeader))); xStyleProps.setPropertyValue("HeaderBodyDistance", 0); } else { xStyleProps.setPropertyValue("TopMargin", Integer .valueOf(Transform.translateHwp2Office(secd.page.marginTop + secd.page.marginHeader))); } } else { xStyleProps.setPropertyValue("TopMargin", Integer.valueOf(Transform.translateHwp2Office(secd.page.marginTop))); xStyleProps.setPropertyValue("HeaderIsOn", false); } // FooterIsOn,FooterText,FooterTextFirst,FooterTextLeft,FooterTextRight,FooterIsShared,FooterIsDynamicHeight,FooterHeight, // FooterDynamicSpacing,FooterBodyDistance, // FooterLeftMargin,FooterRightMargin, // FooterLeftBorder,FooterRightBorder,FooterTopBorder,FooterBottomBorder,FooterBorderDistance, // FooterLeftBorderDistance,FooterRightBorderDistance,FooterTopBorderDistance,FooterBottomBorderDistance, // FooterFillBackground,FooterBackGraphic,FooterBackGraphicLocation,FooterBackColor,FooterBackGraphicURL,FooterBackTransparent,FooterBackGraphicFilter, // FooterFillHatch,FooterFillHatchName,FooterFillStyle, // FooterFillColor,FooterFillColor2, // FooterFillBitmap,FooterFillBitmapName,FooterFillBitmapSizeY,FooterFillBitmapOffsetY,FooterFillBitmapPositionOffsetY,FooterFillBitmapSizeX,FooterFillBitmapStretch, // FooterFillBitmapTile,FooterFillBitmapPositionOffsetX,FooterFillBitmapMode,FooterShadowFormat,FooterFillBitmapOffsetX,FooterFillBitmapLogicalSize,FooterFillBitmapRectanglePoint, // FooterFillTransparence,FooterFillTransparenceGradient,FooterFillTransparenceGradientName, // FooterFillGradient,FooterFillGradientName,FooterFillGradientStepCount, if (secd.hideFooter == false) { if (secd.page.marginFooter >= 0) { xStyleProps.setPropertyValue("BottomMargin", Integer.valueOf(Transform.translateHwp2Office(secd.page.marginBottom))); xStyleProps.setPropertyValue("FooterIsOn", true); // 한컴(marginHeader) = // LO(HeaderHeight+HeaderBodyDistance) xStyleProps.setPropertyValue("FooterHeight", Integer.valueOf(Transform.translateHwp2Office(secd.page.marginFooter))); xStyleProps.setPropertyValue("FooterBodyDistance", 0); } else { xStyleProps.setPropertyValue("BottomMargin", Integer .valueOf(Transform.translateHwp2Office(secd.page.marginBottom + secd.page.marginFooter))); } } else { xStyleProps.setPropertyValue("BottomMargin", Integer.valueOf(Transform.translateHwp2Office(secd.page.marginBottom))); xStyleProps.setPropertyValue("FooterIsOn", false); } // BackColor,BackTransparent, if (secd.paras!=null) { Optional picOp = secd.paras.stream() .filter(para -> para.p!=null) .flatMap(para -> para.p.stream()) .filter(ctrl -> (ctrl!=null) && (ctrl instanceof Ctrl_ShapePic)) .map(ctrl -> (Ctrl_ShapePic)ctrl) .findFirst(); if (picOp.isPresent()) { // DEBUG // DEBUG setBackGraphic(wContext, xStyleProps, picOp.get()); xStyleProps.setPropertyValue("BackTransparent", false); xStyleProps.setPropertyValue("BackGraphicLocation", GraphicLocation.AREA); xStyleProps.setPropertyValue("BackgroundFullSize", false); xStyleProps.setPropertyValue("HeaderBackTransparent", false); xStyleProps.setPropertyValue("HeaderBackGraphicLocation", GraphicLocation.TILED); xStyleProps.setPropertyValue("FooterBackTransparent", true); xStyleProps.setPropertyValue("FooterBackGraphicLocation", GraphicLocation.RIGHT_BOTTOM); } } // BackGraphicURL,BackGraphicFilter,BackGraphicLocation, } catch (Exception e) { e.printStackTrace(); } pageStyleNameMap.put(customIndex, styleName); pageMap.put(customIndex, secd); customIndex++; return styleName; } private static void setBackGraphic(WriterContext wContext, XPropertySet xStyleProps, Ctrl_ShapePic pic) throws Exception { // image ByteArray로 그림 그리기 Object graphicProviderObject = wContext.mMCF.createInstanceWithContext("com.sun.star.graphic.GraphicProvider", wContext.mContext); XGraphicProvider xGraphicProvider = UnoRuntime.queryInterface(XGraphicProvider.class, graphicProviderObject); byte[] imageAsByteArray = null; String imageType = ""; imageAsByteArray = wContext.getBinBytes(pic.binDataID); imageType = wContext.getBinFormat(pic.binDataID); if (imageAsByteArray == null || imageAsByteArray.length == 0) { log.severe("Something Wrong!!!. skip drawing"); return; } PropertyValue[] v = new PropertyValue[2]; v[0] = new PropertyValue(); v[0].Name = "InputStream"; v[0].Value = new ByteArrayToXInputStreamAdapter(imageAsByteArray); v[1] = new PropertyValue(); v[1].Name = "MimeType"; switch (imageType.toLowerCase()) { case "png": v[1].Value = "image/png"; break; case "bmp": v[1].Value = "image/bmp"; break; case "wmf": v[1].Value = "image/x-wmf"; break; case "jpg": v[1].Value = "image/jpeg"; break; case "gif": v[1].Value = "image/gif"; break; case "tif": v[1].Value = "image/tiff"; break; case "svg": v[1].Value = "image/svg+xml"; break; } XGraphic graphic = xGraphicProvider.queryGraphic(v); if (graphic == null) { log.severe("Error loading the image"); } else { /* if (pic.cropLeft>0 || pic.cropRight>0 || pic.cropTop>0 || pic.cropBottom>0) { try { PropertyValue[] pv = new PropertyValue[2]; Path homeDir = wContext.userHomeDir; Path path = Files.createTempFile(homeDir, "H2O_IMG_", "_" + pic.binDataID + ".png"); URL url = path.toFile().toURI().toURL(); String urlString = url.toExternalForm(); pv[0] = new PropertyValue(); pv[0].Name = "URL"; pv[0].Value = urlString; pv[1] = new PropertyValue(); pv[1].Name = "MimeType"; pv[1].Value = "image/png"; xGraphicProvider.storeGraphic(graphic, pv); xStyleProps.setPropertyValue("BackGraphic", graphic); BufferedImage originalImage = ImageIO.read(path.toFile()); Files.delete(path); int orgWidth = pic.iniPicWidth==0 ? pic.iniWidth : pic.iniPicWidth; int imgWidth = originalImage.getWidth(); int imgHeight = originalImage.getHeight(); float hwp2pixelRatio = (float)imgWidth / orgWidth; int cropLeftPixel = (int)(pic.cropLeft*hwp2pixelRatio); int cropTopPixel = (int)(pic.cropTop*hwp2pixelRatio); int cropWidthPixel = (int)((pic.cropRight-pic.cropLeft)*hwp2pixelRatio); int cropHeightPixel = (int)((pic.cropBottom-pic.cropTop)*hwp2pixelRatio); int subLeft = cropLeftPixel>imgWidth ? 0 : cropLeftPixel; int subTop = cropTopPixel>imgHeight ? 0 : cropTopPixel; int subWidth = Math.min(cropWidthPixel, imgWidth-subLeft); int subHeight = Math.min(cropHeightPixel, imgHeight-subTop); BufferedImage subImgage = originalImage.getSubimage(subLeft, subTop, subWidth, subHeight); try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { ImageIO.write(subImgage, "png", baos); imageAsByteArray = baos.toByteArray(); pv[0] = new PropertyValue(); pv[0].Name = "InputStream"; pv[0].Value = new ByteArrayToXInputStreamAdapter(imageAsByteArray); pv[1] = new PropertyValue(); pv[1].Name = "MimeType"; pv[1].Value = "image/png"; XGraphic graphic2 = xGraphicProvider.queryGraphic(pv); xStyleProps.setPropertyValue("BackGraphic", graphic2); } } catch (IOException e) { e.printStackTrace(); } } */ xStyleProps.setPropertyValue("BackGraphic", graphic); Object obj = xStyleProps.getPropertyValue("BackGraphic"); XGraphic xgraphicOut = (XGraphic) UnoRuntime.queryInterface(XGraphic.class, obj); xStyleProps.setPropertyValue("HeaderBackGraphic", xgraphicOut); xStyleProps.setPropertyValue("FooterBackGraphic", graphic); } } public static String makeCustomPageStyle(Ctrl_SectionDef secd) { String styleName = PAGE_STYLE_PREFIX + customIndex; pageStyleNameMap.put(customIndex, styleName); pageMap.put(customIndex, secd); customIndex++; return styleName; } public static void setColumn(WriterContext wContext, Ctrl_ColumnDef cold) { try { XPropertySet xCursorProps = (XPropertySet) UnoRuntime.queryInterface(XPropertySet.class, wContext.mTextCursor); String currentPageStyleName = xCursorProps.getPropertyValue("PageStyleName").toString(); if (currentPageStyleName.equals("")) { currentPageStyleName = "HWP " + ConvPage.getSectionIndex(); } XStyleFamiliesSupplier xSupplier = (XStyleFamiliesSupplier) UnoRuntime .queryInterface(XStyleFamiliesSupplier.class, wContext.mMyDocument); XNameAccess xFamilies = (XNameAccess) UnoRuntime.queryInterface(XNameAccess.class, xSupplier.getStyleFamilies()); XNameContainer xFamily = (XNameContainer) UnoRuntime.queryInterface(XNameContainer.class, xFamilies.getByName("PageStyles")); String[] elementNames = xFamily.getElementNames(); XStyle xCurrentPageStyle = UnoRuntime.queryInterface(XStyle.class, xFamily.getByName(currentPageStyleName)); XPropertySet xCurrentPageStyleProps = (XPropertySet) UnoRuntime.queryInterface(XPropertySet.class, xCurrentPageStyle); XTextColumns xColumns = UnoRuntime.queryInterface(XTextColumns.class, wContext.mMSF.createInstance("com.sun.star.text.TextColumns")); xColumns.setColumnCount(cold.colCount); if (cold.colSzWidths != null && cold.colSzWidths.length > 0) { TextColumn[] aSequence = xColumns.getColumns(); for (int i = 0; i < cold.colSzWidths.length; i++) { if (i == 0) { aSequence[i].LeftMargin = 0; aSequence[i].Width = (cold.colSzWidths[i] - cold.colSzGaps[i]) / 2; // 대략계산 aSequence[i].RightMargin = cold.colSzGaps[i] / 4; // 대략계산 } else if (i == cold.colSzWidths.length - 1) { aSequence[i].LeftMargin = cold.colSzGaps[i - 1] / 4; // 대략계산 aSequence[i].Width = (cold.colSzWidths[i] - cold.colSzGaps[i - 1]) / 2; // 대략계산 aSequence[i].RightMargin = 0; } else { aSequence[i].LeftMargin = cold.colSzGaps[i - 1] / 4; // 대략계산 aSequence[i].Width = (cold.colSzWidths[i] - cold.colSzGaps[i]) / 2; // 대략계산 aSequence[i].RightMargin = cold.colSzGaps[i] / 4; // 대략계산 } } xColumns.setColumns(aSequence); } XPropertySet xTextColumnProps = (XPropertySet) UnoRuntime.queryInterface(XPropertySet.class, xColumns); boolean isAutomatic = (boolean) xTextColumnProps.getPropertyValue("IsAutomatic"); if (isAutomatic) { xTextColumnProps.setPropertyValue("AutomaticDistance", Transform.translateHwp2Office(cold.sameGap)); // IsAutomatic // == // true // 일때만 // valid } xTextColumnProps.setPropertyValue("SeparatorLineColor", cold.colLineColor); // aRGB xTextColumnProps.setPropertyValue("SeparatorLineIsOn", true); xTextColumnProps.setPropertyValue("SeparatorLineRelativeHeight", 100); // cold.separatorType // xTextColumnProps.setPropertyValue("SeparatorLineStyle", 0); xTextColumnProps.setPropertyValue("SeparatorLineVerticalAlignment", VerticalAlignment.TOP); xTextColumnProps.setPropertyValue("SeparatorLineWidth", Transform.toLineWidth(cold.colLineWidth) / 2); xCurrentPageStyleProps.setPropertyValue("TextColumns", xColumns); } catch (Exception e) { e.printStackTrace(); } } public static void setHeaderFooter(WriterContext wContext, Ctrl_HeadFoot hf) { String styleName = null; try { XStyleFamiliesSupplier xSupplier = (XStyleFamiliesSupplier) UnoRuntime .queryInterface(XStyleFamiliesSupplier.class, wContext.mMyDocument); XNameAccess xFamilies = (XNameAccess) UnoRuntime.queryInterface(XNameAccess.class, xSupplier.getStyleFamilies()); XNameContainer xFamily = (XNameContainer) UnoRuntime.queryInterface(XNameContainer.class, xFamilies.getByName("PageStyles")); styleName = PAGE_STYLE_PREFIX + ConvPage.getSectionIndex(); XPropertySet xStyleProps = (XPropertySet) UnoRuntime.queryInterface(XPropertySet.class, xFamily.getByName(styleName)); HwpDoc.section.Page page = getCurrentPage().page; WriterContext context2 = new WriterContext(); context2.mContext = wContext.mContext; context2.mDesktop = wContext.mDesktop; context2.mMCF = wContext.mMCF; context2.mMSF = wContext.mMSF; context2.mMyDocument = wContext.mMyDocument; context2.userHomeDir = wContext.userHomeDir; switch (hf.whichPage) { case ODD: xStyleProps.setPropertyValue("PageStyleLayout", PageStyleLayout.MIRRORED); // 짝수쪽, 홀수쪽 번갈아 header가 나오도록 // 하려면 MIRROED xStyleProps.setPropertyValue("FirstIsShared", true); // 첫번째 페이지에도 header가 나오도록 하려면 FirstIsShaed=true xStyleProps.setPropertyValue((hf.isHeader == true ? "HeaderIsOn" : "FooterIsOn"), true); xStyleProps.setPropertyValue((hf.isHeader == true ? "HeaderIsShared" : "FooterIsShared"), false); xStyleProps.setPropertyValue((hf.isHeader == true ? "HeaderHeight" : "FooterHeight"), Integer.valueOf( Transform.translateHwp2Office(hf.isHeader == true ? page.marginHeader : page.marginFooter))); xStyleProps.setPropertyValue("HeaderIsDynamicHeight", false); // xStyleProps.setPropertyValue("FooterBodyDistance", 0); XText headerTextRight = UnoRuntime.queryInterface(XText.class, xStyleProps.getPropertyValue((hf.isHeader == true ? "HeaderTextRight" : "FooterTextRight"))); XTextCursor headerCursorRight = headerTextRight.createTextCursor(); headerCursorRight.gotoEnd(false); headerCursorRight.gotoStart(true); headerCursorRight.setString(""); headerCursorRight.gotoEnd(false); context2.mText = headerTextRight; context2.mTextCursor = headerCursorRight; HwpCallback callbackOdd = new HwpCallback() { @Override public void onAutoNumber(Ctrl_AutoNumber autoNumber, int paraStyleID, int paraShapeID) { String paraStyleName = ConvPara.getStyleName(paraStyleID); HwpRecord_ParaShape paraShape = wContext.getParaShape((short) paraShapeID); try { XPropertySet paraProps = UnoRuntime.queryInterface(XPropertySet.class, headerCursorRight); paraProps.setPropertyValue("ParaStyleName", paraStyleName); ConvPara.setParagraphProperties(paraProps, paraShape, wContext.getDocInfo().compatibleDoc, ConvPara.PARA_SPACING); // ConvPara.setCharacterProperties(paraProps, charShape); XTextField numField; XPropertySet numFieldProp; switch (autoNumber.numType) { case PAGE: default: numField = UnoRuntime.queryInterface(XTextField.class, wContext.mMSF.createInstance("com.sun.star.text.textfield.PageNumber")); numFieldProp = UnoRuntime.queryInterface(XPropertySet.class, numField); numFieldProp.setPropertyValue("NumberingType", NumberingType.ARABIC); numFieldProp.setPropertyValue("SubType", PageNumberType.CURRENT); break; case TOTAL_PAGE: numField = UnoRuntime.queryInterface(XTextField.class, wContext.mMSF.createInstance("com.sun.star.text.textfield.PageCount")); numFieldProp = UnoRuntime.queryInterface(XPropertySet.class, numField); numFieldProp.setPropertyValue("NumberingType", NumberingType.ARABIC); break; } headerTextRight.insertTextContent(headerCursorRight, numField, false); headerCursorRight.gotoEnd(false); } catch (Exception e) { e.printStackTrace(); } }; @Override public boolean onTab(String info) { return false; }; @Override public boolean onText(String content, int charShapeId, int charPos, boolean append) { return false; } @Override public boolean onParaBreak() { return false; } }; for (HwpParagraph para : hf.paras) { HwpRecurs.printParaRecurs(context2, wContext, para, callbackOdd, 2); } // REMOVE last PARA_BREAK HwpRecurs.removeLastParaBreak(context2.mTextCursor); break; case EVEN: xStyleProps.setPropertyValue("PageStyleLayout", PageStyleLayout.MIRRORED); // 짝수쪽, 홀수쪽 번갈아 header가 나오도록 // 하려면 MIRROED xStyleProps.setPropertyValue("FirstIsShared", true); // 첫번째 페이지에도 header가 나오도록 하려면 FirstIsShaed=true xStyleProps.setPropertyValue((hf.isHeader == true ? "HeaderIsOn" : "FooterIsOn"), true); xStyleProps.setPropertyValue((hf.isHeader == true ? "HeaderIsShared" : "FooterIsShared"), false); xStyleProps.setPropertyValue((hf.isHeader == true ? "HeaderHeight" : "FooterHeight"), Integer.valueOf( Transform.translateHwp2Office(hf.isHeader == true ? page.marginHeader : page.marginFooter))); xStyleProps.setPropertyValue("HeaderIsDynamicHeight", false); XText headerTextLeft = UnoRuntime.queryInterface(XText.class, xStyleProps.getPropertyValue((hf.isHeader == true ? "HeaderTextLeft" : "FooterTextLeft"))); XTextCursor headerCursorLeft = headerTextLeft.createTextCursor(); headerCursorLeft.gotoEnd(false); headerCursorLeft.gotoStart(true); headerCursorLeft.setString(""); headerCursorLeft.gotoEnd(false); context2.mText = headerTextLeft; context2.mTextCursor = headerCursorLeft; HwpCallback callbackEven = new HwpCallback() { @Override public void onAutoNumber(Ctrl_AutoNumber autoNumber, int paraStyleID, int paraShapeID) { String paraStyleName = ConvPara.getStyleName(paraStyleID); HwpRecord_ParaShape paraShape = wContext.getParaShape((short) paraShapeID); try { XPropertySet paraProps = UnoRuntime.queryInterface(XPropertySet.class, headerCursorLeft); paraProps.setPropertyValue("ParaStyleName", paraStyleName); ConvPara.setParagraphProperties(paraProps, paraShape, wContext.getDocInfo().compatibleDoc, ConvPara.PARA_SPACING); // ConvPara.setCharacterProperties(paraProps, charShape); XTextField numField; XPropertySet numFieldProp; switch (autoNumber.numType) { case PAGE: default: numField = UnoRuntime.queryInterface(XTextField.class, wContext.mMSF.createInstance("com.sun.star.text.textfield.PageNumber")); numFieldProp = UnoRuntime.queryInterface(XPropertySet.class, numField); numFieldProp.setPropertyValue("NumberingType", NumberingType.ARABIC); numFieldProp.setPropertyValue("SubType", PageNumberType.CURRENT); break; case TOTAL_PAGE: numField = UnoRuntime.queryInterface(XTextField.class, wContext.mMSF.createInstance("com.sun.star.text.textfield.PageCount")); numFieldProp = UnoRuntime.queryInterface(XPropertySet.class, numField); numFieldProp.setPropertyValue("NumberingType", NumberingType.ARABIC); break; } headerTextLeft.insertTextContent(headerCursorLeft, numField, false); headerCursorLeft.gotoEnd(false); } catch (Exception e) { e.printStackTrace(); } }; @Override public boolean onTab(String info) { return false; }; @Override public boolean onText(String content, int charShapeId, int charPos, boolean append) { return false; } @Override public boolean onParaBreak() { return false; } }; for (HwpParagraph para : hf.paras) { HwpRecurs.printParaRecurs(context2, wContext, para, callbackEven, 2); } // REMOVE last PARA_BREAK HwpRecurs.removeLastParaBreak(context2.mTextCursor); break; case BOTH: PageStyleLayout layout = (PageStyleLayout) xStyleProps.getPropertyValue("PageStyleLayout"); if (layout != PageStyleLayout.MIRRORED) { xStyleProps.setPropertyValue("PageStyleLayout", PageStyleLayout.ALL); // 짝수쪽, 홀수쪽 번갈아 header가 나오도록 // 하려면 MIRRORED } xStyleProps.setPropertyValue((hf.isHeader == true ? "HeaderIsOn" : "FooterIsOn"), true); xStyleProps.setPropertyValue((hf.isHeader == true ? "HeaderIsShared" : "FooterIsShared"), true); xStyleProps.setPropertyValue((hf.isHeader == true ? "HeaderHeight" : "FooterHeight"), Integer.valueOf( Transform.translateHwp2Office(hf.isHeader == true ? page.marginHeader : page.marginFooter))); xStyleProps.setPropertyValue("HeaderIsDynamicHeight", false); XText headerTextBoth = UnoRuntime.queryInterface(XText.class, xStyleProps.getPropertyValue((hf.isHeader == true ? "HeaderText" : "FooterText"))); XTextCursor headerCursorBoth = headerTextBoth.createTextCursor(); if ((hf.isHeader==true && headerDone.contains(secdIndex)==false) || (hf.isHeader==false && footerDone.contains(secdIndex)==false)) { // Header 중복 생성 방지 headerCursorBoth.gotoEnd(false); headerCursorBoth.gotoStart(true); headerCursorBoth.setString(""); headerCursorBoth.gotoEnd(false); context2.mText = headerTextBoth; context2.mTextCursor = headerCursorBoth; HwpCallback callbackBoth = new HwpCallback() { @Override public void onAutoNumber(Ctrl_AutoNumber autoNumber, int paraStyleID, int paraShapeID) { String paraStyleName = ConvPara.getStyleName(paraStyleID); HwpRecord_ParaShape paraShape = wContext.getParaShape((short) paraShapeID); try { XPropertySet paraProps = UnoRuntime.queryInterface(XPropertySet.class, headerCursorBoth); paraProps.setPropertyValue("ParaStyleName", paraStyleName); ConvPara.setParagraphProperties(paraProps, paraShape, wContext.getDocInfo().compatibleDoc, ConvPara.PARA_SPACING); // ConvPara.setCharacterProperties(paraProps, charShape); XTextField numField; XPropertySet numFieldProp; switch (autoNumber.numType) { case PAGE: default: numField = UnoRuntime.queryInterface(XTextField.class, wContext.mMSF.createInstance("com.sun.star.text.textfield.PageNumber")); numFieldProp = UnoRuntime.queryInterface(XPropertySet.class, numField); numFieldProp.setPropertyValue("NumberingType", NumberingType.ARABIC); numFieldProp.setPropertyValue("SubType", PageNumberType.CURRENT); break; case TOTAL_PAGE: numField = UnoRuntime.queryInterface(XTextField.class, wContext.mMSF.createInstance("com.sun.star.text.textfield.PageCount")); numFieldProp = UnoRuntime.queryInterface(XPropertySet.class, numField); numFieldProp.setPropertyValue("NumberingType", NumberingType.ARABIC); break; } headerTextBoth.insertTextContent(headerCursorBoth, numField, false); headerCursorBoth.gotoEnd(false); } catch (Exception e) { e.printStackTrace(); } }; @Override public boolean onTab(String info) { return false; }; @Override public boolean onText(String content, int charShapeId, int charPos, boolean append) { return false; } @Override public boolean onParaBreak() { return false; } }; if (hf.paras != null) { for (HwpParagraph para : hf.paras) { HwpRecurs.printParaRecurs(context2, wContext, para, callbackBoth, 2); } // REMOVE last PARA_BREAK HwpRecurs.removeLastParaBreak(context2.mTextCursor); } if (hf.isHeader==true) { headerDone.add(secdIndex); } if (hf.isHeader==false) { footerDone.add(secdIndex); } } break; } // PN = doc.createInstance("com.sun.star.text.textfield.PageNumber") // PC = doc.createInstance("com.sun.star.text.textfield.PageCount") // PN.NumberingType=4 // PN.SubType="CURRENT" // PC.NumberingType=4 } catch (Exception e) { e.printStackTrace(); } } public static void makeNextPage(WriterContext wContext) { if (secdIndex > 0) { wContext.mText.insertControlCharacter(wContext.mTextCursor, ControlCharacter.PARAGRAPH_BREAK, false); } XPropertySet props = UnoRuntime.queryInterface(XPropertySet.class, wContext.mTextCursor); try { props.setPropertyValue("BreakType", BreakType.PAGE_BEFORE); } catch (IllegalArgumentException | UnknownPropertyException | PropertyVetoException | WrappedTargetException e) { e.printStackTrace(); } } public static void makeNextColumn(WriterContext wContext) { XPropertySet props = UnoRuntime.queryInterface(XPropertySet.class, wContext.mTextCursor); try { props.setPropertyValue("BreakType", BreakType.COLUMN_BEFORE); } catch (IllegalArgumentException | UnknownPropertyException | PropertyVetoException | WrappedTargetException e) { e.printStackTrace(); } } public static void setupPage(WriterContext wContext, Page page) { String customPageStyleName = pageStyleNameMap.get(secdIndex); transSection(customPageStyleName, wContext.mText, wContext.mTextCursor, (secdIndex > 0)); } public static void setupPageTemporary(WriterContext wContext, Page page) { XPropertySet propSet = UnoRuntime.queryInterface(XPropertySet.class, wContext.mTextCursor); try { if (page.landscape) { propSet.setPropertyValue("PageDescName", "Landscape"); } else { propSet.setPropertyValue("PageDescName", "Standard"); } propSet.setPropertyValue("BreakType", BreakType.PAGE_BEFORE); propSet.setPropertyValue("PageNumberOffset", (short) 1); // Page style 변경. // https://wiki.openoffice.org/wiki/Documentation/DevGuide/Text/Page_Layout int leftMargin = Transform.translateHwp2Office(page.marginLeft); int rightMargin = Transform.translateHwp2Office(page.marginRight); int topMargin = Transform.translateHwp2Office(page.marginTop) + Transform.translateHwp2Office(page.marginHeader); int bottomMargin = Transform.translateHwp2Office(page.marginBottom) + Transform.translateHwp2Office(page.marginFooter); setPageMargin(wContext.mMyDocument, wContext.mTextCursor, leftMargin, rightMargin, topMargin, bottomMargin); } catch (IllegalArgumentException | UnknownPropertyException | PropertyVetoException | WrappedTargetException e) { e.printStackTrace(); } } /* * margin value should be 100 times millimeter. ex) 1200 = 12mm margin */ public static void setPageMargin(XTextDocument myDoc, XTextCursor xTCursor, int left, int right, int top, int buttom) { try { XStyleFamiliesSupplier xSupplier = UnoRuntime.queryInterface(XStyleFamiliesSupplier.class, myDoc); XPropertySet xTextCursorProps = UnoRuntime.queryInterface(XPropertySet.class, xTCursor); String pageStyleName = xTextCursorProps.getPropertyValue("PageStyleName").toString(); XNameAccess xFamilies = UnoRuntime.queryInterface(XNameAccess.class, xSupplier.getStyleFamilies()); XNameContainer xFamily = UnoRuntime.queryInterface(XNameContainer.class, xFamilies.getByName("PageStyles")); XStyle xStyle = UnoRuntime.queryInterface(XStyle.class, xFamily.getByName(pageStyleName)); XPropertySet xStyleProps = (XPropertySet) UnoRuntime.queryInterface(XPropertySet.class, xStyle); xStyleProps.setPropertyValue("LeftMargin", Short.valueOf((short) left)); xStyleProps.setPropertyValue("RightMargin", Short.valueOf((short) right)); xStyleProps.setPropertyValue("TopMargin", Short.valueOf((short) top)); xStyleProps.setPropertyValue("BottomMargin", Short.valueOf((short) buttom)); } catch (UnknownPropertyException e1) { e1.printStackTrace(); } catch (WrappedTargetException e1) { e1.printStackTrace(); } catch (NoSuchElementException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (PropertyVetoException e) { e.printStackTrace(); } } public static void transSection(String customPageStyleName, XText xText, XTextCursor cursor, boolean nextPage) { XPropertySet props = UnoRuntime.queryInterface(XPropertySet.class, cursor); try { props.setPropertyValue("PageDescName", customPageStyleName); // props.setPropertyValue("PageNumberOffset", (short)1 ); } catch (IllegalArgumentException | UnknownPropertyException | PropertyVetoException | WrappedTargetException e) { e.printStackTrace(); } } } H2Orestart-0.7.2/source/soffice/ConvPara.java000066400000000000000000001564571476273367000210500ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package soffice; import java.util.HashMap; import java.util.Map; import java.util.logging.Logger; import com.sun.star.awt.FontRelief; import com.sun.star.awt.FontSlant; import com.sun.star.awt.FontStrikeout; import com.sun.star.awt.FontUnderline; import com.sun.star.awt.FontWeight; import com.sun.star.beans.XPropertySet; import com.sun.star.container.XNameAccess; import com.sun.star.container.XNameContainer; import com.sun.star.style.LineSpacing; import com.sun.star.style.LineSpacingMode; import com.sun.star.style.ParagraphAdjust; import com.sun.star.style.TabAlign; import com.sun.star.style.TabStop; import com.sun.star.style.XStyle; import com.sun.star.style.XStyleFamiliesSupplier; import com.sun.star.table.ShadowFormat; import com.sun.star.table.ShadowLocation; import com.sun.star.text.FontEmphasis; import com.sun.star.text.ParagraphVertAlign; import com.sun.star.text.XParagraphCursor; import com.sun.star.uno.Exception; import com.sun.star.uno.UnoRuntime; import HwpDoc.HwpDocInfo.CompatDoc; import HwpDoc.HwpElement.HwpRecord_BorderFill; import HwpDoc.HwpElement.HwpRecord_CharShape; import HwpDoc.HwpElement.HwpRecord_ParaShape; import HwpDoc.HwpElement.HwpRecord_Style; import HwpDoc.HwpElement.HwpRecord_TabDef; import HwpDoc.HwpElement.HwpRecord_CharShape.Outline; import HwpDoc.HwpElement.HwpRecord_CharShape.Shadow; import HwpDoc.HwpElement.HwpRecord_TabDef.Tab; public class ConvPara { private static final Logger log = Logger.getLogger(ConvPara.class.getName()); private static Map paragraphStyleNameMap = new HashMap(); private static Map characterStyleNameMap = new HashMap(); private static final String PARAGRAPH_STYLE_PREFIX = "HWP "; static final double PARABREAK_SPACING = 0.86; static final double PARA_SPACING = 0.85; public static void reset(WriterContext wContext) { deleteCustomStyles(wContext); } private static void deleteCustomStyles(WriterContext wContext) { if (wContext.mMyDocument!=null) { try { XStyleFamiliesSupplier xSupplier = (XStyleFamiliesSupplier)UnoRuntime.queryInterface(XStyleFamiliesSupplier.class, wContext.mMyDocument); XNameAccess xFamilies = (XNameAccess) UnoRuntime.queryInterface (XNameAccess.class, xSupplier.getStyleFamilies()); XNameContainer xParagraphFamily = (XNameContainer) UnoRuntime.queryInterface(XNameContainer.class, xFamilies.getByName("ParagraphStyles")); for (Integer custIndex: paragraphStyleNameMap.keySet()) { log.info("Deleting "+paragraphStyleNameMap.get(custIndex)); if (xParagraphFamily.hasByName(paragraphStyleNameMap.get(custIndex))) { try { xParagraphFamily.removeByName(paragraphStyleNameMap.get(custIndex)); } catch (com.sun.star.lang.DisposedException e) { e.printStackTrace(); } } } } catch (Exception e) { e.printStackTrace(); } } paragraphStyleNameMap.clear(); } public static void makeCustomParagraphStyle(WriterContext wContext, int id, HwpRecord_Style hwpStyle) { HwpRecord_ParaShape paraShape = wContext.getParaShape(hwpStyle.paraShape); HwpRecord_CharShape charShape = wContext.getCharShape(hwpStyle.charShape); try { XStyle xListStyle = UnoRuntime.queryInterface(XStyle.class, wContext.mMSF.createInstance("com.sun.star.style.ParagraphStyle")); XStyleFamiliesSupplier xSupplier = (XStyleFamiliesSupplier)UnoRuntime.queryInterface(XStyleFamiliesSupplier.class, wContext.mMyDocument); XNameAccess xFamilies = (XNameAccess) UnoRuntime.queryInterface (XNameAccess.class, xSupplier.getStyleFamilies()); XNameContainer xFamily = (XNameContainer) UnoRuntime.queryInterface(XNameContainer.class, xFamilies.getByName("ParagraphStyles")); String hwpStyleName = PARAGRAPH_STYLE_PREFIX +" "+id+" "+ hwpStyle.name; if (xFamily.hasByName(hwpStyleName)==false) { xFamily.insertByName (hwpStyleName, xListStyle); } paragraphStyleNameMap.put(id, hwpStyleName); XPropertySet xStyleProps = (XPropertySet) UnoRuntime.queryInterface(XPropertySet.class, xFamily.getByName(hwpStyleName)); if (paraShape!=null) { setParagraphProperties(xStyleProps, paraShape, wContext.getDocInfo().compatibleDoc, PARA_SPACING); } if (charShape!=null) { HwpRecord_BorderFill borderFill = wContext.getBorderFill(charShape.borderFillIDRef); setCharacterProperties(xStyleProps, charShape, borderFill, -1); } // NumberingRules 속성을 설정해야 Style이 변경된다. XPropertySet xCursorProps = (XPropertySet) UnoRuntime.queryInterface(XPropertySet.class, wContext.mTextCursor); xCursorProps.setPropertyValue("ParaStyleName", "Standard"); } catch (Exception e) { e.printStackTrace(); } } public static void makeCustomCharacterStyle(WriterContext wContext, int id, HwpRecord_CharShape charShape) { try { XStyle xListStyle = UnoRuntime.queryInterface(XStyle.class, wContext.mMSF.createInstance("com.sun.star.style.CharacterStyle")); XStyleFamiliesSupplier xSupplier = (XStyleFamiliesSupplier)UnoRuntime.queryInterface(XStyleFamiliesSupplier.class, wContext.mMyDocument); XNameAccess xFamilies = (XNameAccess) UnoRuntime.queryInterface (XNameAccess.class, xSupplier.getStyleFamilies()); XNameContainer xFamily = (XNameContainer) UnoRuntime.queryInterface(XNameContainer.class, xFamilies.getByName("CharacterStyles")); String hwpStyleName = PARAGRAPH_STYLE_PREFIX +" "+id; if (xFamily.hasByName(hwpStyleName)==false) { xFamily.insertByName (hwpStyleName, xListStyle); } characterStyleNameMap.put(id, hwpStyleName); XPropertySet xStyleProps = (XPropertySet) UnoRuntime.queryInterface(XPropertySet.class, xFamily.getByName(hwpStyleName)); if (charShape!=null) { HwpRecord_BorderFill borderFill = wContext.getBorderFill(charShape.borderFillIDRef); setCharacterProperties(xStyleProps, charShape, borderFill, -1); } // NumberingRules 속성을 설정해야 Style이 변경된다. XPropertySet xCursorProps = (XPropertySet) UnoRuntime.queryInterface(XPropertySet.class, wContext.mTextCursor); xCursorProps.setPropertyValue("CharStyleName", "Standard"); } catch (Exception e) { e.printStackTrace(); } } static void setNumberingProperties(XPropertySet xStyleProps, HwpRecord_ParaShape paraShape) { String numberingStyleName = ""; try { switch(paraShape.headingType) { case NONE: xStyleProps.setPropertyValue("NumberingStyleName", "default"); xStyleProps.setPropertyValue("NumberingLevel", (short) 0); break; case OUTLINE: numberingStyleName = ConvNumbering.getOutlineStyleName(); xStyleProps.setPropertyValue("NumberingStyleName", numberingStyleName); xStyleProps.setPropertyValue("NumberingLevel", (short) (paraShape.headingLevel)); break; case NUMBER: log.finest("번호문단ID="+paraShape.headingIdRef + ",문단수준="+paraShape.headingLevel); numberingStyleName = ConvNumbering.numberingStyleNameMap.get((int)paraShape.headingIdRef); xStyleProps.setPropertyValue("NumberingStyleName", numberingStyleName); xStyleProps.setPropertyValue("NumberingLevel", (short) paraShape.headingLevel); break; case BULLET: log.finest("글머리표문단ID="+paraShape.headingIdRef + ",문단수준="+paraShape.headingLevel); numberingStyleName = ConvNumbering.bulletStyleNameMap.get((int)paraShape.headingIdRef); xStyleProps.setPropertyValue("NumberingStyleName", numberingStyleName); xStyleProps.setPropertyValue("NumberingLevel", (short) 0); break; } // 문단번호를 설정하면 들여쓰기가 엉망이된다. redundancy 같지만, 들여쓰기를 위해 Margin 설정값을 넣는다. xStyleProps.setPropertyValue("ParaLeftMargin", Transform.translateHwp2Office(paraShape.marginLeft/2)); xStyleProps.setPropertyValue("ParaRightMargin", Transform.translateHwp2Office(paraShape.marginRight/2)); xStyleProps.setPropertyValue("ParaTopMargin", Transform.translateHwp2Office(paraShape.marginPrev/2)); xStyleProps.setPropertyValue("ParaBottomMargin", Transform.translateHwp2Office(paraShape.marginNext/2)); } catch (Exception e) { e.printStackTrace(); } } static void setParagraphProperties(XPropertySet xStyleProps, HwpRecord_ParaShape paraShape, CompatDoc compat, double preferSpace) { try { ParagraphAdjust align = ParagraphAdjust.BLOCK; switch(paraShape.align) { case LEFT: xStyleProps.setPropertyValue("ParaAdjust", ParagraphAdjust.LEFT); break; case RIGHT: xStyleProps.setPropertyValue("ParaAdjust", ParagraphAdjust.RIGHT); break; case CENTER: xStyleProps.setPropertyValue("ParaAdjust", ParagraphAdjust.CENTER); break; case JUSTIFY: xStyleProps.setPropertyValue("ParaAdjust", ParagraphAdjust.BLOCK); // xStyleProps.setPropertyValue("ParaLastLineAdjust", (short)2); break; case DISTRIBUTE: case DISTRIBUTE_SPACE: xStyleProps.setPropertyValue("ParaAdjust", ParagraphAdjust.BLOCK); xStyleProps.setPropertyValue("ParaLastLineAdjust", (short)2); xStyleProps.setPropertyValue("ParaExpandSingleWord", true); break; } // breakLatinWord // 줄 나눔 기준 영어 단위 (0:단어, 1:하이픈, 2:글자) // breakNonLatinWord // 줄 나눔 기준 한글 단위 (0:어절, 1:글자) // snapToGrid // 편집 용지의 줄 격자 사용 여부 // condense // 공백 최소값 (0%~75%) // widowOrphan // 외톨이줄 보호 여부 // keepWithNext // 다음 문단과 함께 여부 xStyleProps.setPropertyValue("ParaKeepTogether", paraShape.keepWithNext); // pageBreakBefore // 문단 앞에서 항상 쪽 나눔 여부 // verAlign // 세로정렬 (0:글꼴기준, 1:위쪽, 2:가운데, 3:아래) short vertAlign = ParagraphVertAlign.CENTER; switch(paraShape.vertAlign) { case BASELINE: vertAlign = ParagraphVertAlign.BOTTOM; // 한컴의 글꼴기준은 LibreOffice의 BOTTOM break; case TOP: vertAlign = ParagraphVertAlign.TOP; break; case CENTER: vertAlign = ParagraphVertAlign.CENTER; break; case BOTTOM: vertAlign = ParagraphVertAlign.BOTTOM; // 한컴의 BOTTOM은 LibreOffice의 BOTTOM보다 더 아래, BOTTOM이 그나마 가장 유사 break; } xStyleProps.setPropertyValue("ParaVertAlignment", vertAlign); // fontLineHeight // 글꼴에 어울리는 줄 높이 여부 // HeadingType headingType // 문단 머리 모양 종류 (0:없음, 1:개요, 2:번호, 3:글머리표(bullet)) // heading; // 번호 문단 ID(Numbering ID) 또는 글머리표 문단 모양 ID(Bullet ID)참조 값 // headingLevel // 문단 수준 (1수준~7수준) // connect // 문단 테두리 연결 여부 xStyleProps.setPropertyValue("ParaIsConnectBorder", paraShape.connect); // ignoreMargin // 문단 여백 무시 여부 // paraTailShape // 문단 꼬리 모양 // indent // 들여쓰기/내어쓰기. // 들여쓰기(+)는 첫줄을 오른쪽으로 얼마나 이동할지.. ParaFirstLineIndent 로 조정 // 내어쓰기(-)는 두번째줄부터 오른쪽으로 얼마나 이동할지.. LeftMargin +조정하고, 첫줄 -조정 xStyleProps.setPropertyValue("ParaIsAutoFirstLineIndent", false); if (paraShape.indent >= 0) { xStyleProps.setPropertyValue("ParaFirstLineIndent", Transform.translateHwp2Office(paraShape.indent/2)); // marginLeft // 왼쪽 여백 xStyleProps.setPropertyValue("ParaLeftMargin", paraShape.marginLeft<0 ? 0 : Transform.translateHwp2Office(paraShape.marginLeft/2)); } else { xStyleProps.setPropertyValue("ParaFirstLineIndent", Transform.translateHwp2Office(paraShape.indent/2)); // marginLeft // 왼쪽 여백 xStyleProps.setPropertyValue("ParaLeftMargin", paraShape.marginLeft<0 ? 0 : Transform.translateHwp2Office(paraShape.marginLeft/2-paraShape.indent/2)); } // marginRight // 오른쪽 여백 xStyleProps.setPropertyValue("ParaRightMargin", paraShape.marginRight<0 ? 0 : Transform.translateHwp2Office(paraShape.marginRight/2)); // marginPrev // 문단 간격 위 (100 mm) xStyleProps.setPropertyValue("ParaTopMargin", paraShape.marginPrev<0 ? 0 : Transform.translateHwp2Office(paraShape.marginPrev/2)); // marginNext // 문단 간격 아래 xStyleProps.setPropertyValue("ParaBottomMargin", paraShape.marginNext<0 ? 0 : Transform.translateHwp2Office(paraShape.marginNext/2)); // lineSpacing // 줄 간격. 한글2007 이하버전(5.0.2.5 버전 미만)에서 사용. // // percent일때:0%~500%, fixed일때:hpwunit또는 글자수,betweenline일때:hwpunit또는글자수 // lineSpacingType; // 줄간격 종류(0:Percent,1:Fixed,2:BetweenLines,4:AtLeast) LineSpacing lineSpacing = new LineSpacing(); switch(paraShape.lineSpacingType) { case 0x0: lineSpacing.Mode = LineSpacingMode.PROP; // 일반텍스트에서는 lineSpacing을 줄인다. HWP 24pt=8.5mm, LO 24pt=11mm, so delta=2.5/11=22.7% // 텍스트 상자 내, 테이블 내에서는 lineSpacing을 그대로 반영. double scale = 1.0; switch(compat) { case HWP: scale = preferSpace>0.0?preferSpace:PARA_SPACING; break; case MS_WORD: scale = 1.21; break; case OLD_HWP: default: scale = 1.0; } lineSpacing.Height = (short)(paraShape.lineSpacing*scale); break; case 0x1: lineSpacing.Mode = LineSpacingMode.FIX; lineSpacing.Height = (short)(paraShape.lineSpacing/2*0.352778); //예) 값:4600, 한컴:23pt, LO:8.113894mm. (1pt=0.352778mm) break; case 0x2: lineSpacing.Mode = LineSpacingMode.LEADING; lineSpacing.Height = (short)(paraShape.lineSpacing); break; case 0x3: lineSpacing.Mode = LineSpacingMode.MINIMUM; lineSpacing.Height = (short)(paraShape.lineSpacing); break; } log.finest("lineSpacing="+lineSpacing.Height+"("+lineSpacing.Mode+") <= LineSpacing="+paraShape.lineSpacing + "("+paraShape.lineSpacingType+")"); xStyleProps.setPropertyValue("ParaLineSpacing", lineSpacing); // tabDef // 탭 정의 아이디(TabDef ID) 참조 값 HwpRecord_TabDef tabDef = WriterContext.getTabDef(paraShape.tabDef); TabStop[] tss = new TabStop[tabDef.count]; if (tabDef.count>0) { for (int i=0; i= 0) { xStyleProps.setPropertyValue("ParaFirstLineIndent", Transform.translateHwp2Office(paraShape.indent/2)); // marginLeft // 왼쪽 여백 xStyleProps.setPropertyValue("ParaLeftMargin", paraShape.marginLeft<0 ? 0 : Transform.translateHwp2Office(paraShape.marginLeft/2)); } else { xStyleProps.setPropertyValue("ParaFirstLineIndent", Transform.translateHwp2Office(paraShape.indent/2)); // marginLeft // 왼쪽 여백 xStyleProps.setPropertyValue("ParaLeftMargin", paraShape.marginLeft<0 ? 0 : Transform.translateHwp2Office(paraShape.marginLeft/2-paraShape.indent/2)); } // marginRight // 오른쪽 여백 xStyleProps.setPropertyValue("ParaRightMargin", paraShape.marginRight<0 ? 0 : Transform.translateHwp2Office(paraShape.marginRight/2)); // marginPrev // 문단 간격 위 (100 mm) xStyleProps.setPropertyValue("ParaTopMargin", paraShape.marginPrev<0 ? 0 : Transform.translateHwp2Office(paraShape.marginPrev/2)); // marginNext // 문단 간격 아래 xStyleProps.setPropertyValue("ParaBottomMargin", paraShape.marginNext<0 ? 0 : Transform.translateHwp2Office(paraShape.marginNext/2)); // lineSpacing // 줄 간격. 한글2007 이하버전(5.0.2.5 버전 미만)에서 사용. // // percent일때:0%~500%, fixed일때:hpwunit또는 글자수,betweenline일때:hwpunit또는글자수 // lineSpacingType; // 줄간격 종류(0:Percent,1:Fixed,2:BetweenLines,4:AtLeast) LineSpacing lineSpacing = new LineSpacing(); switch(paraShape.lineSpacingType) { case 0x0: lineSpacing.Mode = LineSpacingMode.PROP; // 일반텍스트에서는 lineSpacing을 줄인다. HWP 24pt=8.5mm, LO 24pt=11mm, so delta=2.5/11=22.7% // 텍스트 상자 내, 테이블 내에서는 lineSpacing을 그대로 반영. double scale = 1.0; switch(compat) { case HWP: scale = preferSpace>0.0?preferSpace:PARA_SPACING; break; case MS_WORD: scale = 1.21; break; case OLD_HWP: default: scale = 1.0; } lineSpacing.Height = (short)(paraShape.lineSpacing*scale); break; case 0x1: lineSpacing.Mode = LineSpacingMode.FIX; lineSpacing.Height = (short)(paraShape.lineSpacing/2*0.352778); //예) 값:4600, 한컴:23pt, LO:8.113894mm. (1pt=0.352778mm) break; case 0x2: lineSpacing.Mode = LineSpacingMode.LEADING; lineSpacing.Height = (short)(paraShape.lineSpacing); break; case 0x3: lineSpacing.Mode = LineSpacingMode.MINIMUM; lineSpacing.Height = (short)(paraShape.lineSpacing); break; } log.finest("lineSpacing="+lineSpacing.Height+"("+lineSpacing.Mode+") <= LineSpacing="+paraShape.lineSpacing + "("+paraShape.lineSpacingType+")"); xStyleProps.setPropertyValue("ParaLineSpacing", lineSpacing); // tabDef // 탭 정의 아이디(TabDef ID) 참조 값 HwpRecord_TabDef tabDef = WriterContext.getTabDef(paraShape.tabDef); TabStop[] tss = new TabStop[tabDef.count]; if (tabDef.count>0) { for (int i=0; i=0) { // 위 첨자, 아래 첨자 처리 if (charShape.superScript) { xStyleProps.setPropertyValue("CharAutoEscapement", true); xStyleProps.setPropertyValue("CharEscapement", (short)14000); xStyleProps.setPropertyValue("CharEscapementHeight", (byte)58); } else if (charShape.subScript) { xStyleProps.setPropertyValue("CharAutoEscapement", true); xStyleProps.setPropertyValue("CharEscapement", (short)-14000); xStyleProps.setPropertyValue("CharEscapementHeight", (byte)58); } else { xStyleProps.setPropertyValue("CharEscapement", (short)0); xStyleProps.setPropertyValue("CharEscapementHeight", (byte)100); } } // https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1style_1_1CharacterProperties.html // CharFontName,CharFontStyleName,CharFontFamily,CharFontCharSet,CharFontPitch,CharColor,CharHeight,CharUnderline,CharWeight,CharPosture, // CharAutoKerning,CharBackColor,CharShadingValue,CharBackTransparent,CharCaseMap,CharCrossedOut,CharFlash,CharStrikeout,CharWordMode,CharKerning,CharLocale, // CharKeepTogether,CharNoLineBreak,CharShadowed,CharFontType,CharStyleName,CharContoured,CharCombineIsOn,CharCombinePrefix,CharCombineSuffix,CharEmphasis // CharRelief,RubyText,RubyAdjust,RubyCharStyleName,RubyIsAbove,CharRotation,CharRotationIsFitToLine,CharScaleWidth,HyperLinkURL,HyperLinkTarget,HyperLinkName // VisitedCharStyleName,UnvisitedCharStyleName,CharEscapementHeight,CharNoHyphenation,CharUnderlineColor,CharUnderlineHasColor,CharHidden,TextUserDefinedAttributes // CharLeftBorder,CharRightBorder,CharTopBorder,CharBottomBorder,CharBorderDistance,CharLeftBorderDistance,CharRightBorderDistance,CharTopBorderDistance // CharBottomBorderDistance,CharShadowFormat,CharHighlight,RubyPosition } catch (Exception e) { e.printStackTrace(); } } static void setDrawingCharacterProperties(XPropertySet xStyleProps, HwpRecord_CharShape charShape, int step) { try { xStyleProps.setPropertyValue("CharFontName", charShape.fontName[1]); // paraProps.setPropertyValue("CharFontStyleName", faceName.faceName); xStyleProps.setPropertyValue("CharFontNameAsian", charShape.fontName[0]); // paraProps.setPropertyValue("CharFontStyleNameAsian", faceName.faceName); // charShape.fontID[0]; // 언어별 글꼴ID(FaceID) // f# // charShape.ratio[0]; // 언어별 장평, 50%~200% // r# log.finest("CharWidth="+charShape.ratio[0]); xStyleProps.setPropertyValue("CharScaleWidth", charShape.ratio[0]); // charShape.spacing[0]; // 언어별 자간, -50%~50% // s# // 리브레오피스 자간거리(pt) = y ; (폰트크기(pt)*한컴자간(%) = x ; 가중치 a = 0.85 ; 절편 b = 0.5 double spacing = ((double)charShape.height)/100 * (charShape.spacing[0] / 100.0f) * 0.8 + 0.4; // 1pt = 0.35278mm = 35.278 (1/100 mm) spacing *= 35.278; xStyleProps.setPropertyValue("CharKerning", (short)Math.round(spacing)); // charShape.relSize[0]; // 언어별 상대 크기, 10%~250% // e# // charShape.charOffset[0]; // 언어별 글자 위치, -100%~100% // o# // charShape.height; // 기준 크기, 0pt~4096pt // he xStyleProps.setPropertyValue("CharHeight", (float)charShape.height*(charShape.relSize[1]/100.0f)/100.0f); // 1000 (10.0pt) xStyleProps.setPropertyValue("CharHeightAsian", (float)charShape.height*(charShape.relSize[0]/100.0f)/100.0f); // 1000 (10.0pt) // charShape.bold; // 진하게 여부 // bo if (charShape.bold) { xStyleProps.setPropertyValue("CharWeight", FontWeight.BOLD); xStyleProps.setPropertyValue("CharWeightAsian", FontWeight.BOLD); } else { xStyleProps.setPropertyValue("CharWeight", FontWeight.NORMAL); xStyleProps.setPropertyValue("CharWeightAsian", FontWeight.NORMAL); } // charShape.italic; // 기울임 여부 // it if (charShape.italic) { xStyleProps.setPropertyValue("CharPosture", FontSlant.ITALIC); xStyleProps.setPropertyValue("CharPostureAsian", FontSlant.ITALIC); } else { xStyleProps.setPropertyValue("CharPosture", FontSlant.NONE); xStyleProps.setPropertyValue("CharPostureAsian", FontSlant.NONE); } // charShape.underline; // 밑줄 종류 // ut // charShape.underlineShape; // 밑줄 모양 // us if (charShape.underline!=null) { switch(charShape.underline) { case NONE: xStyleProps.setPropertyValue("CharUnderline", FontUnderline.NONE); break; case BOTTOM: case CENTER: case TOP: switch (charShape.underlineShape) { case SOLID: xStyleProps.setPropertyValue("CharUnderline", FontUnderline.SINGLE); break; case DASH: xStyleProps.setPropertyValue("CharUnderline", FontUnderline.DASH); break; case DOT: xStyleProps.setPropertyValue("CharUnderline", FontUnderline.DOTTED); break; case DASH_DOT: xStyleProps.setPropertyValue("CharUnderline", FontUnderline.DASHDOT); break; case DASH_DOT_DOT: xStyleProps.setPropertyValue("CharUnderline", FontUnderline.DASHDOTDOT); break; case LONG_DASH: xStyleProps.setPropertyValue("CharUnderline", FontUnderline.LONGDASH); break; case DOUBLE_SLIM: xStyleProps.setPropertyValue("CharUnderline", FontUnderline.DOUBLE); break; case CIRCLE: case SLIM_THICK: case THICK_SLIM: case SLIM_THICK_SLIM: xStyleProps.setPropertyValue("CharUnderline", FontUnderline.SINGLE); break; default: break; } break; } } // charShape.underlineColor; // 밑줄 색 xStyleProps.setPropertyValue("CharUnderlineColor", charShape.underlineColor); // charShape.outline; // 외곽선종류 // if (charShape.outline==Outline.NONE) { xStyleProps.setPropertyValue("CharContoured", false); } else { xStyleProps.setPropertyValue("CharContoured", true); } // charShape.emboss; // 양각 여부 // em? // charShape.engrave; // 음각 여부 // en? if (charShape.emboss) { xStyleProps.setPropertyValue("CharRelief", FontRelief.EMBOSSED); } else if (charShape.engrave) { xStyleProps.setPropertyValue("CharRelief", FontRelief.ENGRAVED); } else { xStyleProps.setPropertyValue("CharRelief", FontRelief.NONE); } // charShape.superScript; // 위 첨자 여부 // su? // charShape.subScript; // 아래 첨자 여부 // sb? // charShape.strikeOut; // 취소선 여부 // charShape.strikeOutShape; // 취소선 모양 // charShape.strikeOutColor; // 취소선 색 if (charShape.strikeOut!=0) { switch(charShape.strikeOutShape) { case SOLID: xStyleProps.setPropertyValue("CharStrikeout", FontStrikeout.SINGLE); break; case DASH: xStyleProps.setPropertyValue("CharStrikeout", FontStrikeout.SINGLE); break; case DOT: case DASH_DOT: case DASH_DOT_DOT: case LONG_DASH: xStyleProps.setPropertyValue("CharStrikeout", FontStrikeout.SINGLE); break; case DOUBLE_SLIM: xStyleProps.setPropertyValue("CharStrikeout", FontStrikeout.DOUBLE); break; case CIRCLE: case SLIM_THICK: case THICK_SLIM: case SLIM_THICK_SLIM: xStyleProps.setPropertyValue("CharUnderline", FontUnderline.SINGLE); break; default: break; } } //charShape.symMark; // 강조점 종류 xStyleProps.setPropertyValue("CharEmphasis", FontEmphasis.NONE); //charShape.useFontSpace; // 글꼴에 어울리는 빈칸 사용 여부 // uf? //charShape.useKerning; // kerning여부 // uk? //charShape.textColor; // 글자 색 // xStyleProps.setPropertyValue("CharColor", charShape.textColor); //charShape.shadeColor; // 음영 색 if (charShape.shadeColor != 0xFFFFFFFF) { xStyleProps.setPropertyValue("CharBackColor", charShape.shadeColor); } // charShape.shadow; // 그림자 종류 // // charShape.shadowSpacing; // 그림자 간격, -100%~100% // charShape.shadowColor; // 그림자 색 if (charShape.shadow!=Shadow.NONE) { xStyleProps.setPropertyValue("CharShadowed", true); } // charShape.borderFillId; // 글자 테두리/배경 ID(CharShapeBorderFill ID) 참조 값 // https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1style_1_1CharacterProperties.html // CharFontName,CharFontStyleName,CharFontFamily,CharFontCharSet,CharFontPitch,CharColor,CharEscapement,CharHeight,CharUnderline,CharWeight,CharPosture, // CharAutoKerning,CharBackColor,CharShadingValue,CharBackTransparent,CharCaseMap,CharCrossedOut,CharFlash,CharStrikeout,CharWordMode,CharKerning,CharLocale, // CharKeepTogether,CharNoLineBreak,CharShadowed,CharFontType,CharStyleName,CharContoured,CharCombineIsOn,CharCombinePrefix,CharCombineSuffix,CharEmphasis // CharRelief,RubyText,RubyAdjust,RubyCharStyleName,RubyIsAbove,CharRotation,CharRotationIsFitToLine,CharScaleWidth,HyperLinkURL,HyperLinkTarget,HyperLinkName // VisitedCharStyleName,UnvisitedCharStyleName,CharEscapementHeight,CharNoHyphenation,CharUnderlineColor,CharUnderlineHasColor,CharHidden,TextUserDefinedAttributes // CharLeftBorder,CharRightBorder,CharTopBorder,CharBottomBorder,CharBorderDistance,CharLeftBorderDistance,CharRightBorderDistance,CharTopBorderDistance // CharBottomBorderDistance,CharShadowFormat,CharHighlight,RubyPosition } catch (Exception e) { e.printStackTrace(); } } public static void setDefaultParaStyle(WriterContext wContext) { try { XParagraphCursor xParaCursor = (XParagraphCursor) UnoRuntime.queryInterface(XParagraphCursor.class, wContext.mTextCursor); xParaCursor.gotoEnd(false); XPropertySet xParaProps = (XPropertySet) UnoRuntime.queryInterface(XPropertySet.class, xParaCursor); String styleName = getStyleName(0); xParaProps.setPropertyValue ("ParaStyleName", styleName); xParaProps.setPropertyValue ("NumberingStyleName", "default"); xParaProps.setPropertyValue ("NumberingLevel", (short) 0); } catch (Exception e) { e.printStackTrace(); } } static String getStyleName(int styleID) { return paragraphStyleNameMap.get(styleID); } static String getCharStyleName(int styleID) { return characterStyleNameMap.get(styleID); } } H2Orestart-0.7.2/source/soffice/ConvTable.java000066400000000000000000002442051476273367000212010ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package soffice; import java.io.IOException; import java.util.*; import java.util.function.Function; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.IntStream; import com.sun.star.uno.*; import com.sun.star.uno.Exception; import HwpDoc.HwpElement.HwpRecord_BorderFill; import HwpDoc.HwpElement.HwpRecord_CharShape; import HwpDoc.HwpElement.HwpRecord_ParaShape; import HwpDoc.HwpElement.HwpRecord_Style; import HwpDoc.paragraph.Ctrl; import HwpDoc.paragraph.Ctrl_AutoNumber; import HwpDoc.paragraph.Ctrl_Character; import HwpDoc.paragraph.Ctrl_Table; import HwpDoc.paragraph.HwpParagraph; import HwpDoc.paragraph.ParaText; import HwpDoc.paragraph.TblCell; import soffice.HwpCallback.TableFrame; import com.sun.star.lang.*; import com.sun.star.lang.IllegalArgumentException; import com.sun.star.table.BorderLine2; import com.sun.star.table.BorderLineStyle; import com.sun.star.table.TableBorder; import com.sun.star.table.XCell; import com.sun.star.table.XTableRows; import com.sun.star.awt.Size; import com.sun.star.beans.PropertyVetoException; import com.sun.star.beans.UnknownPropertyException; import com.sun.star.beans.XPropertySet; import com.sun.star.drawing.TextVerticalAdjust; import com.sun.star.drawing.XShape; import com.sun.star.frame.*; import com.sun.star.text.*; public class ConvTable { private static final Logger log = Logger.getLogger(ConvTable.class.getName()); public static XDesktop xDesktop; public static XMultiServiceFactory xMSF; public static XMultiComponentFactory xMCF; public static XComponent doc; private static int autoNum = 0; public static void reset(WriterContext wContext) { autoNum = 0; } public static void endParagraph(XTextCursor cursor) { append(cursor, ControlCharacter.PARAGRAPH_BREAK); } public static void appendPara(XTextCursor cursor, String text) { append(cursor, text); append(cursor, ControlCharacter.PARAGRAPH_BREAK); } public static void append(XTextCursor cursor, String text) { cursor.setString(text); cursor.gotoEnd(false); } public static void append(XTextCursor cursor, short ctrlChar) { XText xText = cursor.getText(); xText.insertControlCharacter(cursor, ctrlChar, false); } public static void insertTable(WriterContext wContext, Ctrl_Table table, short paraShapeID, HwpCallback callback, int step) { // 테이블 그리기 전, 문단모양 설정한다. 문단에 Frame을 넣기때문에 문단 margin에 맞게 여백이 들어가야 한다. HwpRecord_ParaShape paraShape = wContext.getParaShape((short) paraShapeID); XParagraphCursor paraCursor = UnoRuntime.queryInterface(XParagraphCursor.class, wContext.mTextCursor); XPropertySet paraProps = UnoRuntime.queryInterface(XPropertySet.class, paraCursor); ConvPara.setParagraphProperties(paraProps, paraShape, wContext.getDocInfo().compatibleDoc, ConvPara.PARA_SPACING); XTextFrame xFrame = null; XText xFrameText = null; XTextCursor xFrameCursor = null; try { TblCell[][] cellArray = new TblCell[table.nRows][table.nCols]; for (int index = 0; index < table.cells.size(); index++) { TblCell cell = table.cells.get(index); cellArray[cell.rowAddr][cell.colAddr] = cell; } // HWP테이블에는 존재하지 않는 Cell이 포함되어 있는 경우가 있다. // 이 경우 TableColumnSeparator의 Position 계산이 어려워질뿐 아니라 CellMerge도 어려워진다. all null // 칼럼을 제거하고, colSpan을 조정한다. cellArray = removeAllNullColumns(cellArray); int maxColSize = Arrays.stream(cellArray).map(row -> row.length) .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())) .entrySet().stream() .max(Map.Entry.comparingByValue()).get().getKey(); Object tableObj = wContext.mMSF.createInstance("com.sun.star.text.TextTable"); XTextTable xTextTable = UnoRuntime.queryInterface(XTextTable.class, tableObj); if (xTextTable == null) { log.severe("Could not create a text table."); return; } XPropertySet tableProps = UnoRuntime.queryInterface(XPropertySet.class, xTextTable); xTextTable.initialize(cellArray.length, maxColSize); if (callback != null && callback.onTableWithFrame() == TableFrame.MAKE) { xFrame = makeOuterFrame(wContext, table); xFrameText = xFrame.getText(); xFrameCursor = xFrameText.createTextCursor(); xFrameText.insertTextContent(xFrameCursor, xTextTable, false); if (wContext.version >= 72) { XPropertySet frameProps = UnoRuntime.queryInterface(XPropertySet.class, xFrame); TextContentAnchorType anchorType = (TextContentAnchorType) frameProps.getPropertyValue("AnchorType"); if (anchorType == TextContentAnchorType.AT_PARAGRAPH) { xFrameText.insertString(xFrameCursor, " ", true); } } addCaptionString(wContext, xFrameText, xFrameCursor, table, step + 1); step += 1; } else { wContext.mText.insertTextContent(wContext.mTextCursor, xTextTable, false); if (wContext.version >= 72) { TextContentAnchorType anchorType = (TextContentAnchorType) tableProps.getPropertyValue("AnchorType"); // 한컴에서 AS_CHAR 가 아니면 PARA_BREAK가 추가되어 있을것임, libreoffice에서는 ANCHOR를 붙일수 없으니 마지막 // PARA_BREAK를 제거하자. // 여기서는 없애지 못하므로 (아래의 코드로도 PARA_BREAK 지우지 못한다. Table 이후 나오는 PARA_BREAK 한개를 생략한다. // (oweParaBreak) if (table.treatAsChar == false) { HwpRecurs.removeLastParaBreak(wContext.mTextCursor); } } // 캡션이 표시될 문단의 오른쪽 spacing을 임의로 조정 HwpDoc.section.Page page = ConvPage.getCurrentPage().page; int rightSpace = page.width - page.marginLeft - page.marginRight - paraShape.marginLeft - table.width; addCaptionString(wContext, wContext.mText, wContext.mTextCursor, table, 0, rightSpace, step); if (callback != null && callback.onTableWithFrame() == TableFrame.MADE) { } else { setParaPosition(tableProps, table, paraShape); } if (table.treatAsChar) { tableProps.setPropertyValue("Split", false); } else { if ((table.attr & 0x03) == 0x02) { tableProps.setPropertyValue("Split", true); } else { tableProps.setPropertyValue("Split", false); } } } TableBorder tBorder = new TableBorder(); tBorder.LeftLine = Transform.toBorderLine((HwpRecord_BorderFill.Border) null); tBorder.IsLeftLineValid = true; tBorder.RightLine = Transform.toBorderLine((HwpRecord_BorderFill.Border) null); tBorder.IsRightLineValid = true; tBorder.TopLine = Transform.toBorderLine((HwpRecord_BorderFill.Border) null); tBorder.IsTopLineValid = true; tBorder.BottomLine = Transform.toBorderLine((HwpRecord_BorderFill.Border) null); tBorder.IsBottomLineValid = true; tBorder.VerticalLine = Transform.toBorderLine((HwpRecord_BorderFill.Border) null); tBorder.IsVerticalLineValid = true; tBorder.HorizontalLine = Transform.toBorderLine((HwpRecord_BorderFill.Border) null); tBorder.IsHorizontalLineValid = true; tableProps.setPropertyValue("TableBorder", tBorder); int width = Transform.translateHwp2Office(table.width); short sTableColumnRelativeSum = (Short) tableProps.getPropertyValue("TableColumnRelativeSum"); double dRatio = (double) sTableColumnRelativeSum / (double) width; // adjust cell width // prepare column width values, so that adjust cell width. int[] colWidth = calculateColumnWidth(cellArray, table.width); for (int n = 0; n < colWidth.length; n++) { colWidth[n] = Transform.translateHwp2Office(colWidth[n]); } TableColumnSeparator[] xSeparators = UnoRuntime.queryInterface(TableColumnSeparator[].class, tableProps.getPropertyValue("TableColumnSeparators")); double dPosition = 0; for (int col = 0; col < colWidth.length; col++) { dPosition += (colWidth[col] * dRatio > 1.0 ? colWidth[col] * dRatio : 1); // [20221106] position gap이 최소 // 1이상이 되도록 한다. 장기요양 서식. xSeparators[col].Position = (short) Math.ceil(dPosition); } tableProps.setPropertyValue("TableColumnSeparators", xSeparators); // table row 높이 조정 XTableRows xTableRows = xTextTable.getRows(); if (xTableRows != null) { int[] rowHeight = calculateRowHeight(cellArray, table.height); if (hasNullRow(rowHeight, table.height) == false) { for (int row = 0; row < xTableRows.getCount(); row++) { Object aRowObj = xTableRows.getByIndex(row); XPropertySet tableRowProps = UnoRuntime.queryInterface(com.sun.star.beans.XPropertySet.class, aRowObj); tableRowProps.setPropertyValue("IsAutoHeight", false); tableRowProps.setPropertyValue("Height", Transform.translateHwp2Office(rowHeight[row])); } } } // merge cell and fill text. for (int row = 0; row < cellArray.length; row++) { for (int col = cellArray[row].length - 1; col >= 0; col--) { TblCell cell = cellArray[row][col]; String cellAddr = mkCellNameBeforeMerge(cellArray, col, row, row - 1); XCell xCell = xTextTable.getCellByName(cellAddr); if (xCell != null) { // 인접한 Cell Border때문에 border가 지워지지 않는 현상있음. 모든 Cell에 대해 border가 없는 상태로 먼저 만든다. XPropertySet cellProps = UnoRuntime.queryInterface(XPropertySet.class, xCell); HwpRecord_BorderFill.Border nullBorder = null; cellProps.setPropertyValue("LeftBorder", Transform.toBorderLine(nullBorder)); cellProps.setPropertyValue("RightBorder", Transform.toBorderLine(nullBorder)); cellProps.setPropertyValue("TopBorder", Transform.toBorderLine(nullBorder)); cellProps.setPropertyValue("BottomBorder", Transform.toBorderLine(nullBorder)); // 테이블 경계선 근처에서 텍스트 잘리지 않도록 함 cellProps.setPropertyValue("LeftBorderDistance", 0); cellProps.setPropertyValue("RightBorderDistance", 0); cellProps.setPropertyValue("TopBorderDistance", 0); cellProps.setPropertyValue("BottomBorderDistance", 0); cellProps.setPropertyValue("BorderDistance", 0); } if (cell == null) continue; if (cell.colSpan > 1 || cell.rowSpan > 1) { try { // 한가지 방식으로 통일하면 좋겠으나 extension에서 동작하지 않아 각각 방식을 달리한다. log.finest("CELL ADDR=" + cellAddr + " out of (" + cell.colAddr + ", " + cell.rowAddr + ") spans (" + cell.colSpan + "," + cell.rowSpan + ")"); XTextTableCursor xCellCursor = xTextTable.createCursorByCellName(cellAddr); // column merge는 goRight() 후 mergeRange(). gotoCellByName()은 extension에서 동작 안함. if (cell.colSpan > 1) { boolean ret = xCellCursor.goRight((short) (cell.colSpan - 1), true); log.finest("GoRight(" + (cell.colSpan - 1) + ") return=" + ret); ret = xCellCursor.mergeRange(); log.finest("Merge=" + ret); } // row merge는 gotoCellByName() 후 mergeRange(). goRight()은 extension에서 동작 안함. if (cell.rowSpan > 1) { String cellAddr2 = mkCellNameBeforeMerge(cellArray, cell.colAddr + cell.colSpan - 1, cell.rowAddr + cell.rowSpan - 1, row - 1); boolean ret = xCellCursor.gotoCellByName(cellAddr2, true); log.finest("GotoCell(" + cellAddr2 + ") return=" + ret); ret = xCellCursor.mergeRange(); log.finest("Merge=" + ret); } } catch (com.sun.star.uno.RuntimeException e1) { e1.printStackTrace(); } } try { // 셀을 병합한 후에 Border를 그린다. if (xCell != null) { XPropertySet cellProps = UnoRuntime.queryInterface(XPropertySet.class, xCell); HwpRecord_BorderFill cellBorderFill = WriterContext.getBorderFill(cell.borderFill); if (cellBorderFill != null) { cellProps.setPropertyValue("LeftBorder", Transform.toBorderLine(cellBorderFill.left)); cellProps.setPropertyValue("RightBorder", Transform.toBorderLine(cellBorderFill.right)); cellProps.setPropertyValue("TopBorder", Transform.toBorderLine(cellBorderFill.top)); cellProps.setPropertyValue("BottomBorder", Transform.toBorderLine(cellBorderFill.bottom)); } // 안쪽여백은 한컴값을 사용하지 않고, 0으로 임의조정한다. 가능한 Cell내 모든 문단을 보여주기 위해 cellProps.setPropertyValue("LeftBorderDistance", 0 /* Transform.translateHwp2Office(table.inLSpace) */); cellProps.setPropertyValue("RightBorderDistance", 0 /* Transform.translateHwp2Office(table.inRSpace) */); cellProps.setPropertyValue("TopBorderDistance", 0 /* Transform.translateHwp2Office(table.inUSpace) */); cellProps.setPropertyValue("BottomBorderDistance", 0 /* Transform.translateHwp2Office(table.inDSpace) */); cellProps.setPropertyValue("VertOrient", Transform.toVertAlign(cell.verAlign.ordinal())); if (cellBorderFill != null) { if (cellBorderFill.fill.isColorFill()) { if (callback != null && callback.onTableWithFrame() == TableFrame.MAKE_PART) { cellProps.setPropertyValue("BackTransparent", true); // ZOrder 변경이 안되어 이런식으로 만듬. } else { cellProps.setPropertyValue("BackTransparent", false); cellProps.setPropertyValue("BackColor", cellBorderFill.fill.faceColor); } } else if (cellBorderFill.fill.isGradFill()) { // CellProperties에는 Gradient 그릴 수 있는 속성이 없다. 중간색으로 칠한다. if (cellBorderFill.fill.colors.length==2) { short r, g, b; r = (short) (((cellBorderFill.fill.colors[0]>>16&0x00FF) + (cellBorderFill.fill.colors[1]>>16&0x00FF))/2); g = (short) (((cellBorderFill.fill.colors[0]>>8&0x00FF) + (cellBorderFill.fill.colors[1]>>8&0x00FF))/2); b = (short) (((cellBorderFill.fill.colors[0]&0x00FF) + (cellBorderFill.fill.colors[1]&0x00FF))/2); int midColor = (r<<16)|(g<<8)|b; cellProps.setPropertyValue("BackColor", midColor); } } else if (cellBorderFill.fill.isImageFill()) { ConvGraphics.fillGraphic(wContext, cellProps, cellBorderFill.fill); } else { cellProps.setPropertyValue("BackTransparent", false); } } } } catch (IllegalArgumentException | UnknownPropertyException | PropertyVetoException | WrappedTargetException e) { e.printStackTrace(); } setCellPara(cellAddr, cell, xTextTable, table, wContext, callback, step); } } } catch (IllegalArgumentException | Exception e3) { e3.printStackTrace(); } } private static XTextFrame makeOuterFrame(WriterContext wContext, Ctrl_Table table) throws Exception { XTextFrame xFrame = null; Object oFrame = wContext.mMSF.createInstance("com.sun.star.text.TextFrame"); xFrame = (XTextFrame) UnoRuntime.queryInterface(XTextFrame.class, oFrame); if (xFrame == null) { log.severe("Could not create a text frame."); return xFrame; } XShape tfShape = UnoRuntime.queryInterface(XShape.class, xFrame); tfShape.setSize( new Size(Transform.translateHwp2Office(table.width), Transform.translateHwp2Office(table.height))); XPropertySet frameProps = UnoRuntime.queryInterface(XPropertySet.class, xFrame); setFramePosition(frameProps, table); setFrameWrapStyle(frameProps, table); BorderLine2 frameBorder = new BorderLine2(); frameBorder.Color = 0x000000; frameBorder.LineStyle = BorderLineStyle.NONE; frameBorder.InnerLineWidth = 0; frameBorder.OuterLineWidth = 0; frameBorder.LineDistance = 0; frameBorder.LineWidth = 0; frameProps.setPropertyValue("TopBorder", frameBorder); frameProps.setPropertyValue("BottomBorder", frameBorder); frameProps.setPropertyValue("LeftBorder", frameBorder); frameProps.setPropertyValue("RightBorder", frameBorder); // margin 0으로 frameProps.setPropertyValue("LeftMargin", 0); frameProps.setPropertyValue("RightMargin", 0); frameProps.setPropertyValue("TopMargin", 0); frameProps.setPropertyValue("BottomMargin", 0); // 안쪽여백을 0으로... frameProps.setPropertyValue("BorderDistance", 0); XText xText = wContext.mTextCursor.getText(); xText.insertTextContent(wContext.mTextCursor, xFrame, false); if (wContext.version >= 72) { TextContentAnchorType anchorType = (TextContentAnchorType) frameProps.getPropertyValue("AnchorType"); if (anchorType == TextContentAnchorType.AT_PARAGRAPH) { xText.insertString(wContext.mTextCursor, " ", false); } } // 2024.12.25 테이블 이후 PARA_BRAEK 보이지 않도록 고정크기로 하기 위해 false로 변경하고, TOP align으로 변경 // 높이 고정시에 캡션이 보이지 않으므로, 캡션이 있으면 true로 하자. if (table.caption == null || table.caption.size()==0) { frameProps.setPropertyValue("FrameIsAutomaticHeight", false); } else { frameProps.setPropertyValue("FrameIsAutomaticHeight", true); } frameProps.setPropertyValue("TextVerticalAdjust", TextVerticalAdjust.TOP); wContext.mTextCursor.gotoEnd(false); return xFrame; } private static void setParaPosition(XPropertySet xProps, Ctrl_Table shape, HwpRecord_ParaShape paraShape) { int posX = 0; int posY = 0; HwpDoc.section.Page page = ConvPage.getCurrentPage().page; if (shape.treatAsChar == true) { posX = Transform.translateHwp2Office(shape.horzOffset) - Transform.translateHwp2Office(page.marginLeft); posX = Math.max(posX, 0); try { xProps.setPropertyValue("LeftMargin", posX); xProps.setPropertyValue("Width", Transform.translateHwp2Office(shape.width)); switch (paraShape.align) { case LEFT: // 왼쪽 정렬 xProps.setPropertyValue("HoriOrient", HoriOrientation.LEFT); break; case RIGHT: // 오른쪽 정렬 xProps.setPropertyValue("HoriOrient", HoriOrientation.RIGHT); break; case CENTER: // 가운데 정렬 xProps.setPropertyValue("HoriOrient", HoriOrientation.CENTER); break; case JUSTIFY: // 양쪽 정렬 case DISTRIBUTE: // 배분 정렬 case DISTRIBUTE_SPACE: // 나눔 정렬 xProps.setPropertyValue("HoriOrient", HoriOrientation.NONE); break; } } catch (IllegalArgumentException | UnknownPropertyException | PropertyVetoException | WrappedTargetException e) { e.printStackTrace(); } } else { try { switch (shape.vertRelTo) { case PARA: switch (shape.vertAlign) { case TOP: posY = Transform.translateHwp2Office(shape.vertOffset); if (posY > 0) { xProps.setPropertyValue("TopMargin", posY); } break; } break; } } catch (IllegalArgumentException | UnknownPropertyException | PropertyVetoException | WrappedTargetException e) { e.printStackTrace(); } try { switch (shape.horzRelTo) { case PAPER: switch (shape.horzAlign) { case LEFT: // LEFT case INSIDE: posX = Transform.translateHwp2Office(shape.horzOffset) - Transform.translateHwp2Office(page.marginLeft); posX = Math.max(posX, 0); xProps.setPropertyValue("LeftMargin", posX); xProps.setPropertyValue("HoriOrient", HoriOrientation.LEFT_AND_WIDTH); xProps.setPropertyValue("Width", Transform.translateHwp2Office(shape.width)); break; case CENTER: posX = Transform.translateHwp2Office(page.width) / 2 + Transform.translateHwp2Office(shape.horzOffset); posX = posX - Transform.translateHwp2Office(page.marginLeft); posX = posX - Transform.translateHwp2Office(shape.width) / 2; xProps.setPropertyValue("LeftMargin", posX); xProps.setPropertyValue("HoriOrient", HoriOrientation.LEFT_AND_WIDTH); xProps.setPropertyValue("Width", Transform.translateHwp2Office(shape.width)); break; case RIGHT: // RIGHT case OUTSIDE: posX = Transform.translateHwp2Office(page.width) - Transform.translateHwp2Office(page.marginLeft) - Transform.translateHwp2Office(shape.horzOffset); posX = posX - Transform.translateHwp2Office(shape.width); xProps.setPropertyValue("LeftMargin", posX); xProps.setPropertyValue("HoriOrient", HoriOrientation.LEFT_AND_WIDTH); xProps.setPropertyValue("Width", Transform.translateHwp2Office(shape.width)); break; } break; case PAGE: switch (shape.horzAlign) { case LEFT: // LEFT case INSIDE: posX = Transform.translateHwp2Office(shape.horzOffset); posX = Math.max(posX, 0); xProps.setPropertyValue("LeftMargin", posX); xProps.setPropertyValue("HoriOrient", HoriOrientation.LEFT_AND_WIDTH); xProps.setPropertyValue("Width", Transform.translateHwp2Office(shape.width)); break; case CENTER: posX = (Transform.translateHwp2Office(page.width) - Transform.translateHwp2Office(page.marginLeft) - Transform.translateHwp2Office(page.marginRight)) / 2; posX = posX + Transform.translateHwp2Office(shape.horzOffset) - Transform.translateHwp2Office(shape.width) / 2; xProps.setPropertyValue("LeftMargin", posX); xProps.setPropertyValue("HoriOrient", HoriOrientation.LEFT_AND_WIDTH); xProps.setPropertyValue("Width", Transform.translateHwp2Office(shape.width)); break; case RIGHT: // RIGHT case OUTSIDE: posX = Transform.translateHwp2Office(page.width) - Transform.translateHwp2Office(page.marginLeft) - Transform.translateHwp2Office(page.marginRight); posX = posX - Transform.translateHwp2Office(shape.horzOffset) - Transform.translateHwp2Office(shape.width); xProps.setPropertyValue("LeftMargin", posX); xProps.setPropertyValue("HoriOrient", HoriOrientation.LEFT_AND_WIDTH); xProps.setPropertyValue("Width", Transform.translateHwp2Office(shape.width)); break; } break; case COLUMN: case PARA: switch (shape.horzAlign) { case LEFT: // LEFT. 왼쪽맞춤 일때 width,rightMargin이 필요(width+rightMargin이 전체 Para Width). 왼쪽에서부터 // 일때 width,leftMargin이 필요(width+leftMargin이 전체 width일 필요 없음). case INSIDE: posX = Transform.translateHwp2Office(shape.horzOffset) + Transform.translateHwp2Office(paraShape.marginLeft / 2); xProps.setPropertyValue("LeftMargin", posX); xProps.setPropertyValue("HoriOrient", HoriOrientation.LEFT_AND_WIDTH); xProps.setPropertyValue("Width", Transform.translateHwp2Office(shape.width)); break; case CENTER: // 가운데맞춤일때 width,leftMargin이 필요 (width+leftMagin+leftMargin이 전체 width. // leftMargin=rightMargin 으로 봄) posX = (Transform.translateHwp2Office(page.width) - Transform.translateHwp2Office(page.marginLeft) - Transform.translateHwp2Office(page.marginRight)) / 2; posX = posX + Transform.translateHwp2Office(shape.horzOffset) - Transform.translateHwp2Office(shape.width) / 2; xProps.setPropertyValue("LeftMargin", posX); xProps.setPropertyValue("HoriOrient", HoriOrientation.LEFT_AND_WIDTH); xProps.setPropertyValue("Width", Transform.translateHwp2Office(shape.width)); break; case RIGHT: // RIGHT. 오른쪽맞춤 일때, width, leftMargin이 필요(width+leftMargin이 전체 Para Width 이어야 함) case OUTSIDE: posX = Transform.translateHwp2Office(page.width) - Transform.translateHwp2Office(page.marginLeft) - Transform.translateHwp2Office(page.marginRight); posX = posX - Transform.translateHwp2Office(shape.horzOffset) - Transform.translateHwp2Office(shape.width); xProps.setPropertyValue("LeftMargin", posX); xProps.setPropertyValue("HoriOrient", HoriOrientation.LEFT_AND_WIDTH); xProps.setPropertyValue("Width", Transform.translateHwp2Office(shape.width)); break; } break; } } catch (IllegalArgumentException | UnknownPropertyException | PropertyVetoException | WrappedTargetException e) { e.printStackTrace(); } } } private static void setParaWrapStyle(XPropertySet xPropSet, Ctrl_Table shape) { // wrapStyle; // 0:어울림, 1:자리차지, 2:글 뒤로, 3:글 앞으로 // wrapText; // 0:양쪽, 1:왼쪽, 2:오른쪽, 3:큰쪽 try { switch (shape.textWrap) { case SQUARE: // 어울림 WrapTextMode wrapText = WrapTextMode.NONE; switch (shape.textFlow) { case 0x0: // 양쪽 wrapText = WrapTextMode.PARALLEL; break; case 0x1: // 왼쪽 wrapText = WrapTextMode.LEFT; break; case 0x2: // 오른쪽 wrapText = WrapTextMode.RIGHT; break; case 0x3: // 큰쪽 wrapText = WrapTextMode.DYNAMIC; break; } xPropSet.setPropertyValue("TextWrap", wrapText); break; case TOP_AND_BOTTOM: // 자리차지 xPropSet.setPropertyValue("TextWrap", WrapTextMode.NONE); break; case BEHIND_TEXT: // 글 뒤로 xPropSet.setPropertyValue("TextWrap", WrapTextMode.THROUGH); break; case IN_FRONT_OF_TEXT: // 글 앞으로 xPropSet.setPropertyValue("TextWrap", WrapTextMode.THROUGH); break; } } catch (IllegalArgumentException | UnknownPropertyException | PropertyVetoException | WrappedTargetException e) { e.printStackTrace(); } } private static void setFramePosition(XPropertySet xProps, Ctrl_Table table) { int posX = 0; int posY = 0; try { if (table.treatAsChar == true) { xProps.setPropertyValue("AnchorType", TextContentAnchorType.AS_CHARACTER); xProps.setPropertyValue("VertOrient", VertOrientation.CENTER); // Top, Bottom, Center, fromBottom // xProps.setPropertyValue("VertOrientPosition", posY); xProps.setPropertyValue("VertOrientRelation", RelOrientation.TEXT_LINE); // Base line, Character, Row xProps.setPropertyValue("HoriOrient", HoriOrientation.CENTER); // 0:NONE=From left xProps.setPropertyValue("HoriOrientRelation", RelOrientation.PRINT_AREA); // 1:paragraph text area } else { HwpDoc.section.Page page = ConvPage.getCurrentPage().page; switch (table.vertRelTo) { case PAPER: // Anchor to Page xProps.setPropertyValue("AnchorType", TextContentAnchorType.AT_PARAGRAPH); switch (table.vertAlign) { case TOP: xProps.setPropertyValue("VertOrientRelation", RelOrientation.PAGE_FRAME); // 7:EntirePage, // 8:PageTextArea if (table.vertOffset == 0) { xProps.setPropertyValue("VertOrient", VertOrientation.TOP); // 1:Top, 2:Bottom, 2:Center, // 0:NONE(From top) } else { xProps.setPropertyValue("VertOrient", VertOrientation.NONE); // 0:NONE=From top // page상단으로부터 frame상단까지의 offset posY = Transform.translateHwp2Office(table.vertOffset); xProps.setPropertyValue("VertOrientPosition", posY); } break; case CENTER: xProps.setPropertyValue("VertOrientRelation", RelOrientation.PAGE_FRAME); // 7:EntirePage, // 8:PageTextArea if (table.vertOffset == 0) { xProps.setPropertyValue("VertOrient", VertOrientation.CENTER); // 1:Top, 2:Bottom, 2:Center, // 0:NONE(From top) } else { xProps.setPropertyValue("VertOrient", VertOrientation.NONE); // 0:NONE=From top // 중간지점에서 frame 중심까지의 offset -> page상단부터의 frame상단까지 offset으로 계산 posY = (Transform.translateHwp2Office(page.height) - Transform.translateHwp2Office(table.height)) / 2 + Transform.translateHwp2Office(table.vertOffset); xProps.setPropertyValue("VertOrientPosition", posY); } break; case BOTTOM: xProps.setPropertyValue("VertOrientRelation", RelOrientation.PAGE_FRAME); // 7:EntirePage, // 8:PageTextArea if (table.vertOffset == 0) { xProps.setPropertyValue("VertOrient", VertOrientation.BOTTOM); // 1:Top, 2:Bottom, 2:Center, // 0:NONE(From top) } else { xProps.setPropertyValue("VertOrient", VertOrientation.NONE); // 0:NONE=From top // page하단에서 frame 하단까지의 offset을 -> page상단부터의 frame상단까지 offset으로 계산 posY = Transform.translateHwp2Office(page.height) - Transform.translateHwp2Office(table.height) - Transform.translateHwp2Office(table.vertOffset); xProps.setPropertyValue("VertOrientPosition", posY); } break; } break; case PAGE: // 그림에서 AnchorType을 AT_PAGE로 줄때 crash 발생 xProps.setPropertyValue("AnchorType", TextContentAnchorType.AT_PARAGRAPH); switch (table.vertAlign) { case TOP: xProps.setPropertyValue("VertOrientRelation", RelOrientation.PAGE_PRINT_AREA); // 7:EntirePage, // 8:PageTextArea if (table.vertOffset == 0) { xProps.setPropertyValue("VertOrient", VertOrientation.TOP); // 1:Top, 2:Bottom, 2:Center, // 0:NONE(From top) } else { xProps.setPropertyValue("VertOrient", VertOrientation.NONE); // 0:NONE=From top // page상단으로부터 frame상단까지의 offset posY = Transform.translateHwp2Office(table.vertOffset); xProps.setPropertyValue("VertOrientPosition", posY); } break; case CENTER: xProps.setPropertyValue("VertOrientRelation", RelOrientation.PAGE_PRINT_AREA); // 7:EntirePage, // 8:PageTextArea if (table.vertOffset == 0) { xProps.setPropertyValue("VertOrient", VertOrientation.CENTER); // 1:Top, 2:Bottom, 2:Center, // 0:NONE(From top) } else { xProps.setPropertyValue("VertOrient", VertOrientation.NONE); // 0:NONE=From top // 중간지점에서 frame 중심까지의 offset -> page상단부터의 frame상단까지 offset으로 계산 int pageHeight = Transform.translateHwp2Office(page.height) - Transform.translateHwp2Office(page.marginTop) - Transform.translateHwp2Office(page.marginBottom); posY = (pageHeight - Transform.translateHwp2Office(table.height)) / 2; posY += Transform.translateHwp2Office(table.vertOffset); xProps.setPropertyValue("VertOrientPosition", posY); } break; case BOTTOM: xProps.setPropertyValue("VertOrientRelation", RelOrientation.PAGE_PRINT_AREA); // 7:EntirePage, // 8:PageTextArea if (table.vertOffset == 0) { xProps.setPropertyValue("VertOrient", VertOrientation.BOTTOM); // 1:Top, 2:Bottom, 2:Center, // 0:NONE(From top) } else { xProps.setPropertyValue("VertOrient", VertOrientation.NONE); // 0:NONE=From top // 쪽 하단에서 frame 하단까지의 offset을 -> 쪽 상단부터의 frame상단까지 offset으로 계산 int pageHeight = Transform.translateHwp2Office(page.height) - Transform.translateHwp2Office(page.marginTop) - Transform.translateHwp2Office(page.marginBottom); posY = pageHeight - Transform.translateHwp2Office(table.height) - Transform.translateHwp2Office(table.vertOffset); xProps.setPropertyValue("VertOrientPosition", posY); } break; } break; case PARA: xProps.setPropertyValue("AnchorType", TextContentAnchorType.AT_PARAGRAPH); switch (table.vertAlign) { case TOP: xProps.setPropertyValue("VertOrientRelation", RelOrientation.PRINT_AREA); // 7:EntirePage, // 8:PageTextArea if (table.vertOffset == 0) { xProps.setPropertyValue("VertOrient", VertOrientation.TOP); // 1:Top, 2:Bottom, 2:Center, // 0:NONE(From top) } else { xProps.setPropertyValue("VertOrient", VertOrientation.NONE); // 0:NONE=From top // para상단으로부터 frame상단까지의 offset posY = Transform.translateHwp2Office(table.vertOffset); xProps.setPropertyValue("VertOrientPosition", posY); } break; } break; } switch (table.horzRelTo) { case PAPER: switch (table.horzAlign) { case LEFT: // LEFT case INSIDE: xProps.setPropertyValue("HoriOrientRelation", RelOrientation.PAGE_FRAME); // 7:EntirePage, // 8:PageTextArea if (table.horzOffset == 0) { xProps.setPropertyValue("HoriOrient", HoriOrientation.LEFT); // 1:Top, 2:Bottom, 2:Center, // 0:NONE(From top) } else { xProps.setPropertyValue("HoriOrient", HoriOrientation.NONE); // 0:NONE=From top // page상단으로부터 frame상단까지의 offset posX = Transform.translateHwp2Office(table.horzOffset); xProps.setPropertyValue("HoriOrientPosition", posX); } break; case CENTER: xProps.setPropertyValue("HoriOrientRelation", RelOrientation.PAGE_FRAME); // 7:EntirePage, // 8:PageTextArea if (table.horzOffset == 0) { xProps.setPropertyValue("HoriOrient", HoriOrientation.CENTER); // 1:Top, 2:Bottom, 2:Center, // 0:NONE(From top) } else { xProps.setPropertyValue("HoriOrient", HoriOrientation.NONE); // 0:NONE=From top // 중간지점에서 frame 중심까지의 offset -> page상단부터의 frame상단까지 offset으로 계산 posX = (Transform.translateHwp2Office(page.width) - Transform.translateHwp2Office(table.width)) / 2 + Transform.translateHwp2Office(table.horzOffset); xProps.setPropertyValue("HoriOrientPosition", posX); } break; case RIGHT: // RIGHT case OUTSIDE: xProps.setPropertyValue("HoriOrientRelation", RelOrientation.PAGE_FRAME); // 7:EntirePage, // 8:PageTextArea if (table.horzOffset == 0) { xProps.setPropertyValue("HoriOrient", HoriOrientation.RIGHT); // 1:Top, 2:Bottom, 2:Center, // 0:NONE(From top) } else { xProps.setPropertyValue("HoriOrient", HoriOrientation.NONE); // 0:NONE=From top // page하단에서 frame 하단까지의 offset을 -> page상단부터의 frame상단까지 offset으로 계산 posX = Transform.translateHwp2Office(page.width) - Transform.translateHwp2Office(table.width) - Transform.translateHwp2Office(table.horzOffset); xProps.setPropertyValue("HoriOrientPosition", posX); } break; } break; case PAGE: switch (table.horzAlign) { case LEFT: // LEFT case INSIDE: xProps.setPropertyValue("HoriOrientRelation", RelOrientation.PAGE_PRINT_AREA); // 7:EntirePage, // 8:PageTextArea if (table.horzOffset == 0) { xProps.setPropertyValue("HoriOrient", HoriOrientation.LEFT); // 1:Top, 2:Bottom, 2:Center, // 0:NONE(From top) } else { xProps.setPropertyValue("HoriOrient", HoriOrientation.NONE); // 0:NONE=From left // page상단으로부터 frame상단까지의 offset posX = Transform.translateHwp2Office(table.horzOffset); xProps.setPropertyValue("HoriOrientPosition", posX); } break; case CENTER: xProps.setPropertyValue("HoriOrientRelation", RelOrientation.PAGE_PRINT_AREA); // 7:EntirePage, // 8:PageTextArea if (table.horzOffset == 0) { xProps.setPropertyValue("HoriOrient", HoriOrientation.CENTER); // 1:Top, 2:Bottom, 2:Center, // 0:NONE(From top) } else { xProps.setPropertyValue("HoriOrient", HoriOrientation.NONE); // 0:NONE=From left // 중간지점에서 frame 중심까지의 offset -> page상단부터의 frame상단까지 offset으로 계산 int pageWidth = Transform.translateHwp2Office(page.width) - Transform.translateHwp2Office(page.marginLeft) - Transform.translateHwp2Office(page.marginRight); posX = (pageWidth - Transform.translateHwp2Office(table.width)) / 2; posX += Transform.translateHwp2Office(table.horzOffset); xProps.setPropertyValue("HoriOrientPosition", posX); } break; case RIGHT: // RIGHT case OUTSIDE: xProps.setPropertyValue("HoriOrientRelation", RelOrientation.PAGE_PRINT_AREA); // 7:EntirePage, // 8:PageTextArea if (table.horzOffset == 0) { xProps.setPropertyValue("HoriOrient", HoriOrientation.RIGHT); // 1:Top, 2:Bottom, 2:Center, // 0:NONE(From top) } else { xProps.setPropertyValue("HoriOrient", HoriOrientation.NONE); // 0:NONE=From left // page하단에서 frame 하단까지의 offset을 -> page상단부터의 frame상단까지 offset으로 계산 int pageWidth = Transform.translateHwp2Office(page.width) - Transform.translateHwp2Office(page.marginLeft) - Transform.translateHwp2Office(page.marginRight); posX = pageWidth - Transform.translateHwp2Office(table.width) - Transform.translateHwp2Office(table.horzOffset); xProps.setPropertyValue("HoriOrientPosition", posX); } break; } break; case COLUMN: case PARA: switch (table.horzAlign) { case LEFT: // LEFT case INSIDE: xProps.setPropertyValue("HoriOrientRelation", RelOrientation.PRINT_AREA); // 1:paragraph text // area if (table.horzOffset == 0) { xProps.setPropertyValue("HoriOrient", HoriOrientation.LEFT); // 1:Top, 2:Bottom, 2:Center, // 0:NONE(From top) } else { xProps.setPropertyValue("HoriOrient", HoriOrientation.NONE); // 0:NONE=From left // page상단으로부터 frame상단까지의 offset posX = Transform.translateHwp2Office(table.horzOffset); xProps.setPropertyValue("HoriOrientPosition", posX); } break; case CENTER: xProps.setPropertyValue("HoriOrientRelation", RelOrientation.PRINT_AREA); // 7:EntirePage, // 8:PageTextArea if (table.horzOffset == 0) { xProps.setPropertyValue("HoriOrient", HoriOrientation.CENTER); // 1:Top, 2:Bottom, 2:Center, // 0:NONE(From top) } else { xProps.setPropertyValue("HoriOrient", HoriOrientation.NONE); // 0:NONE=From left // 중간지점에서 frame 중심까지의 offset -> page상단부터의 frame상단까지 offset으로 계산 int pageWidth = Transform.translateHwp2Office(page.width) - Transform.translateHwp2Office(page.marginLeft) - Transform.translateHwp2Office(page.marginRight); posX = (pageWidth - Transform.translateHwp2Office(table.width)) / 2; posX += Transform.translateHwp2Office(table.horzOffset); xProps.setPropertyValue("HoriOrientPosition", posX); } break; case RIGHT: // RIGHT case OUTSIDE: xProps.setPropertyValue("HoriOrientRelation", RelOrientation.PRINT_AREA); // 7:EntirePage, // 8:PageTextArea if (table.horzOffset == 0) { xProps.setPropertyValue("HoriOrient", HoriOrientation.RIGHT); // 1:Top, 2:Bottom, 2:Center, // 0:NONE(From top) } else { xProps.setPropertyValue("HoriOrient", HoriOrientation.NONE); // 0:NONE=From left // page하단에서 frame 하단까지의 offset을 -> page상단부터의 frame상단까지 offset으로 계산 int pageWidth = Transform.translateHwp2Office(page.width) - Transform.translateHwp2Office(page.marginLeft) - Transform.translateHwp2Office(page.marginRight); posX = pageWidth - Transform.translateHwp2Office(table.width) - Transform.translateHwp2Office(table.horzOffset); xProps.setPropertyValue("HoriOrientPosition", posX); } break; } break; } } } catch (IllegalArgumentException | UnknownPropertyException | PropertyVetoException | WrappedTargetException e) { e.printStackTrace(); } } private static void setFrameWrapStyle(XPropertySet xPropSet, Ctrl_Table shape) { // wrapStyle; // 0:어울림, 1:자리차지, 2:글 뒤로, 3:글 앞으로 // wrapText; // 0:양쪽, 1:왼쪽, 2:오른쪽, 3:큰쪽 try { switch (shape.textWrap) { case SQUARE: // 어울림 xPropSet.setPropertyValue("Opaque", true); xPropSet.setPropertyValue("AllowOverlap", true); // THROUGH에서는 효과 없음. WrapTextMode wrapText = WrapTextMode.NONE; switch (shape.textFlow) { case 0x0: // 양쪽 wrapText = WrapTextMode.PARALLEL; break; case 0x1: // 왼쪽 wrapText = WrapTextMode.LEFT; break; case 0x2: // 오른쪽 wrapText = WrapTextMode.RIGHT; break; case 0x3: // 큰쪽 wrapText = WrapTextMode.DYNAMIC; break; } if (shape.treatAsChar == false) { try { xPropSet.setPropertyValue("SurroundContour", false);// contour는 THROUGH에서는 효과 없음 } catch (UnknownPropertyException e) { e.printStackTrace(); } } try { xPropSet.setPropertyValue("TextWrap", wrapText); } catch (UnknownPropertyException e) { e.printStackTrace(); } break; case TOP_AND_BOTTOM: // 자리차지 xPropSet.setPropertyValue("Opaque", true); if (shape.treatAsChar == false) { try { xPropSet.setPropertyValue("AllowOverlap", true); // THROUGH에서는 효과 없음. } catch (UnknownPropertyException e) { e.printStackTrace(); } } try { xPropSet.setPropertyValue("TextWrap", WrapTextMode.NONE); } catch (UnknownPropertyException e) { e.printStackTrace(); } break; case BEHIND_TEXT: // 글 뒤로 xPropSet.setPropertyValue("Opaque", false); if (shape.treatAsChar == false) { try { xPropSet.setPropertyValue("AllowOverlap", true); // THROUGH에서는 효과 없음. xPropSet.setPropertyValue("IsAutomaticContour", false); } catch (UnknownPropertyException e) { e.printStackTrace(); } } try { xPropSet.setPropertyValue("TextWrap", WrapTextMode.THROUGH); } catch (UnknownPropertyException e) { e.printStackTrace(); } break; case IN_FRONT_OF_TEXT: // 글 앞으로 xPropSet.setPropertyValue("Opaque", true); if (shape.treatAsChar == false) { try { xPropSet.setPropertyValue("AllowOverlap", true); // THROUGH에서는 효과 없음. } catch (UnknownPropertyException e) { e.printStackTrace(); } } try { xPropSet.setPropertyValue("TextWrap", WrapTextMode.DYNAMIC); } catch (UnknownPropertyException e) { e.printStackTrace(); } break; } // xPropSet.setPropertyValue("ZOrder", shape.zOrder); } catch (UnknownPropertyException | PropertyVetoException | WrappedTargetException e) { e.printStackTrace(); } } static void addCaptionString(WriterContext wContext, XText xFrameText, XTextCursor xFrameCursor, Ctrl_Table table, int step) { addCaptionString(wContext, xFrameText, xFrameCursor, table, 0, 0, step); } static void addCaptionString(WriterContext wContext, XText xFrameText, XTextCursor xFrameCursor, Ctrl_Table table, int leftSpacing, int rightSpacing, int step) { if (table.caption == null || table.caption.size() == 0) return; XParagraphCursor paraCursor = UnoRuntime.queryInterface(XParagraphCursor.class, xFrameCursor); XPropertySet paraProps = UnoRuntime.queryInterface(XPropertySet.class, paraCursor); List capStr = new ArrayList(); short[] charShapeID = new short[1]; Optional ctrlOp = table.caption.stream().filter(c -> c.p != null) .flatMap(c -> c.p.stream()) .filter(c -> c instanceof ParaText).findFirst(); if (ctrlOp.isPresent()) { charShapeID[0] = (short) ((ParaText) ctrlOp.get()).charShapeId; } HwpCallback callback = new HwpCallback() { @Override public void onAutoNumber(Ctrl_AutoNumber autoNumber, int paraStyleID, int paraShapeID) { capStr.add(Integer.toString(++autoNum)); }; @Override public boolean onTab(String info) { capStr.add("\t"); return true; }; @Override public boolean onText(String content, int charShapeId, int charPos, boolean append) { capStr.add(content); charShapeID[0] = (short) charShapeId; return true; } @Override public boolean onParaBreak() { capStr.add("\r"); return true; } }; HwpRecurs.printParaRecurs(wContext, wContext, table.caption.get(0), callback, step); if (capStr.size() > 0) { if (step > 2) { // 본문위에 table은 step=2, Frame내에 들어간 table은 step=3 if (capStr.get(capStr.size() - 1).equals("\r")) { // 마지막이 PARA_BREAK라면 출력하지 않음. capStr.remove(capStr.size() - 1); } } String styleName = ConvPara.getStyleName((int) table.caption.get(0).paraStyleID); HwpRecord_ParaShape captionParaShape = wContext.getParaShape(table.caption.get(0).paraShapeID); HwpRecord_CharShape captionCharShape = wContext.getCharShape(charShapeID[0]); try { paraProps.setPropertyValue("ParaStyleName", styleName); ConvPara.setParagraphProperties(paraProps, captionParaShape, wContext.getDocInfo().compatibleDoc, ConvPara.PARA_SPACING); HwpRecord_BorderFill borderFill = wContext.getBorderFill(captionCharShape.borderFillIDRef); ConvPara.setCharacterProperties(paraProps, captionCharShape, borderFill, step); paraProps.setPropertyValue("ParaTopMargin", Transform.translateHwp2Office(table.captionSpacing)); paraProps.setPropertyValue("ParaBottomMargin", Transform.translateHwp2Office(table.captionSpacing)); paraProps.setPropertyValue("ParaLeftMargin", Transform.translateHwp2Office(leftSpacing)); paraProps.setPropertyValue("ParaRightMargin", Transform.translateHwp2Office(rightSpacing)); for (String cap : capStr) { xFrameText.insertString(xFrameCursor, cap, false); } } catch (Exception e) { e.printStackTrace(); } } } public static String mkCellNameBeforeMerge(TblCell[][] cellArray, int x, int y, int mergedY) { // cell에 대한 알파벳을 가져오려면, // null을 포함하고 (아직 머지하기 전의 알파벳을 구해야 하므로), nullColCount // 이전 row에서 rowpan으로 인한 colspan 분량을 빼고, otherRowsColSpan // 현재 row에서 자리 차지하는 알파벳 갯수 증가. curRowsCount int nullColCount = 0; for (int col = 0; col < x; col++) { if (cellArray[y][col] == null) { nullColCount++; } } int otherRowsColSpan = Arrays.stream(cellArray).flatMap(row -> Arrays.stream(row)) .filter(cell -> cell != null && cell.rowAddr <= mergedY && cell.colAddr <= x && cell.rowAddr + (cell.rowSpan - 1) >= y) .map(cell -> Integer.valueOf(cell.colSpan)).reduce(0, Integer::sum); int curRowsCount = (int) Arrays.stream(cellArray).flatMap(row -> Arrays.stream(row)) .filter(cell -> cell != null) .filter(cell -> (cell.rowAddr <= mergedY && cell.rowAddr + (cell.rowSpan - 1) >= y && cell.colAddr < x) || (cell.rowAddr == y && cell.colAddr < x)) .count(); int xx = (nullColCount + curRowsCount - otherRowsColSpan) / 26; int xxx = (nullColCount + curRowsCount - otherRowsColSpan) % 26; return (xx > 0 ? "" + ((char) ('a' + (xxx))) : "" + ((char) ('A' + xxx))) + (y + 1); } private static void setCellPara(String cellName, TblCell cell, XTextTable xtable, Ctrl_Table table, WriterContext wContext, HwpCallback cb, int step) { XCell xCell = xtable.getCellByName(cellName); if (xCell == null) { log.fine("Can't get CellName:" + cellName); return; } XText cellText = UnoRuntime.queryInterface(XText.class, xCell); if (cellText == null) { log.fine("Can't get CellText"); return; } XTextCursor cellCursor = cellText.createTextCursor(); if (cellCursor == null) return; WriterContext childContext = new WriterContext(); childContext.mContext = wContext.mContext; childContext.mDesktop = wContext.mDesktop; childContext.mMCF = wContext.mMCF; childContext.mMSF = wContext.mMSF; childContext.mMyDocument = wContext.mMyDocument; childContext.userHomeDir = wContext.userHomeDir; childContext.mText = cellText; childContext.mTextCursor = cellCursor; // parabreak 앞으로 이동 cellCursor.gotoStart(false); for (int paraIndex = 0; paraIndex < cell.paras.size(); paraIndex++) { boolean isLastPara = (paraIndex == cell.paras.size() - 1) ? true : false; HwpParagraph para = cell.paras.get(paraIndex); String styleName = ConvPara.getStyleName((int) para.paraStyleID); log.finer("StyleID=" + para.paraStyleID + ", StyleName=" + styleName); if (styleName == null || styleName.isEmpty()) { log.fine("Style Name is empty"); } short[] charShapeID = new short[1]; if (para.p != null) { Optional ctrlOp = para.p.stream().filter(ctrl -> ctrl!=null).findFirst(); if (ctrlOp.isPresent()) { Ctrl ctrl = ctrlOp.get(); if (ctrl instanceof ParaText) { charShapeID[0] = (short) ((ParaText) ctrlOp.get()).charShapeId; } else if (ctrl instanceof Ctrl_Character) { charShapeID[0] = (short) ((Ctrl_Character) ctrlOp.get()).charShapeId; } } } HwpRecord_Style paraStyle = wContext.getParaStyle(para.paraStyleID); HwpRecord_ParaShape paraShape = wContext.getParaShape(para.paraShapeID); HwpRecord_CharShape charShape = wContext.getCharShape(charShapeID[0]); if (para.p == null || para.p.size() == 0) { if (isLastPara == false) { HwpRecurs.insertParaString(childContext, "\r", para.lineSegs, styleName, paraStyle, paraShape, charShape, true, true, step); } continue; } HwpCallback callback = new HwpCallback() { @Override public void onAutoNumber(Ctrl_AutoNumber autoNumber, int paraStyleID, int paraShapeID) { }; @Override public boolean onTab(String info) { HwpRecurs.insertParaString(childContext, "\t", para.lineSegs, styleName, paraStyle, paraShape, charShape, true, true, step); return true; }; @Override public boolean onText(String content, int charShapeId, int charPos, boolean append) { String styleNameTemp = ConvPara.getStyleName((int) para.paraStyleID); HwpRecord_Style paraStyleTemp = wContext.getParaStyle(para.paraStyleID); HwpRecord_ParaShape paraShape = wContext.getParaShape(para.paraShapeID); HwpRecord_ParaShape paraShapeTemp = null; try { paraShapeTemp = HwpRecord_ParaShape.clone(paraShape); if (cell.paras.size()==1) { // Cell내 문단이 1개만 있는 경우, 선 간격을 최소로 한다. LibreOffice와 Hwp 간격 차이를 해소하기 위함. paraShapeTemp.lineSpacing = 100; paraShapeTemp.lineSpacingType = 0x3; } } catch (java.lang.ClassNotFoundException | IOException e) { e.printStackTrace(); } HwpRecord_CharShape charShapeTemp = HwpRecord_CharShape.clone(wContext.getCharShape((short) charShapeId)); // 테이블내에서는 자간을 원래(-50%~50%)보다 작게(-60%~47%) 변경해서 쓴다. 육안으로 크게 차이가 나지 않는 범위내에서 테이블 셀에 가능한 모두 보이도록 하기 위함 for (int i=0; i allNullColList = new ArrayList(); int rowSize = cellArray.length; int maxColSize = Arrays.stream(cellArray).map(row -> row.length) .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())).entrySet().stream() .max(Map.Entry.comparingByValue()).get().getKey(); for (int col = 0; col < maxColSize; col++) { int nCol = col; boolean exists = IntStream.range(0, rowSize).mapToObj(r -> cellArray[r][nCol]).anyMatch(c -> c != null); if (exists == false) { allNullColList.add(col); } } if (allNullColList.size() > 0) { // Collections.reverse(allNullColList); newCellArray = new TblCell[cellArray.length][maxColSize - allNullColList.size()]; for (int oldCol = 0, newCol = 0; oldCol < maxColSize; oldCol++) { if (allNullColList.contains(oldCol) == false) { // all null 칼럼이 아니면, for (int row = 0; row < newCellArray.length; row++) { newCellArray[row][newCol] = cellArray[row][oldCol]; if (newCellArray[row][newCol] != null) { newCellArray[row][newCol].rowAddr = (short) row; newCellArray[row][newCol].colAddr = (short) newCol; } } newCol++; } else { // all null 칼럼이라면, for (int row = 0; row < newCellArray.length; row++) { log.finest("Removing null column. originalCol=" + oldCol + ", row=" + row); if (newCol > 0) { int nRow = row, nNewCol = newCol; Optional lastColOp = Arrays.stream(newCellArray[row]).filter(c -> c != null) .filter(c -> c.colAddr < nNewCol).map(c -> Integer.valueOf(c.colAddr)) .reduce(Integer::max); if (lastColOp.isPresent()) { int lastCol = lastColOp.get(); if (newCellArray[row][lastCol].colSpan > 1) { newCellArray[row][lastCol].colSpan -= 1; } } } } } } } else { newCellArray = cellArray; } return newCellArray; } private static int calculateEquasion(List list, int[] colWidth, int col, int span, boolean isColumn) { int ret = 0; if (span == 1) { for (TblCell cell : list) { if (isColumn) { if (cell.width > 0 && cell.colSpan == 1 && cell.colAddr == col) { ret = cell.width; break; } } else { if (cell.height > 0 && cell.rowSpan == 1 && cell.rowAddr == col) { ret = cell.height; break; } } } } else { // 예) span==5 일때 // [-4] [-3] [-2] [-1] [ 0] [ ] [ ] [ ] [ ] // [ ] [-3] [-2] [-1] [ 0] [ 1] [ ] [ ] [ ] // [ ] [ ] [-2] [-1] [ 0] [ 1] [ 2] [ ] [ ] // [ ] [ ] [ ] [-1] [ 0] [ 1] [ 2] [ 3] [ ] // [ ] [ ] [ ] [ ] [ 0] [ 1] [ 2] [ 3] [ 4] // int span_width[] = new int[5]; // for (TblCell cell: list) { // if (cell.width>0 && cell.colSpan==5 && cell.colAddr==col-4) { // span_width[0] = cell.width; // } else if (cell.width>0 && cell.colSpan==5 && cell.colAddr==col-3) { // span_width[1] = cell.width; // } else if (cell.width>0 && cell.colSpan==5 && cell.colAddr==col-2) { // span_width[2] = cell.width; // } else if (cell.width>0 && cell.colSpan==5 && cell.colAddr==col-1) { // span_width[3] = cell.width; // } else if (cell.width>0 && cell.colSpan==5 && cell.colAddr==col) { // span_width[4] = cell.width; // } // } // if (span_width[0]>0 && colWidth[col-4]>0 && colWidth[col-3]>0 && // colWidth[col-2]>0 && colWidth[col-1]>0) { // ret = span_width[0] - colWidth[col-4] - colWidth[col-3] - colWidth[col-2] - // colWidth[col-1]; // } else if (span_width[1]>0 && colWidth[col-3]>0 && colWidth[col-2]>0 && // colWidth[col-1]>0 && colWidth[col+1]>0) { // ret = span_width[1] - colWidth[col-3] - colWidth[col-2] - colWidth[col-1] - // colWidth[col+1]; // } else if (span_width[2]>0 && colWidth[col-2]>0 && colWidth[col-1]>0 && // colWidth[col+1]>0 && colWidth[col+2]>0) { // ret = span_width[2] - colWidth[col-2] - colWidth[col-1] - colWidth[col+1] - // colWidth[col+2]; // } else if (span_width[3]>0 && colWidth[col-1]>0 && colWidth[col+1]>0 && // colWidth[col+2]>0 && colWidth[col+3]>0) { // ret = span_width[3] - colWidth[col-1] - colWidth[col+1] - colWidth[col+2] - // colWidth[col+3]; // } else if (span_width[4]>0 && colWidth[col+1]>0 && colWidth[col+2]>0 && // colWidth[col+3]>0 && colWidth[col+4]>0) { // ret = span_width[4] - colWidth[col+1] - colWidth[col+2] - colWidth[col+3] - // colWidth[col+4]; // } int span_width[] = new int[span]; for (TblCell cell : list) { for (int idx = 0; idx < span; idx++) { if (isColumn) { if (cell.width > 0 && cell.colSpan == span && cell.colAddr == col - (span - 1) + idx) { span_width[idx] = cell.width; } } else { if (cell.height > 0 && cell.rowSpan == span && cell.rowAddr == col - (span - 1) + idx) { span_width[idx] = cell.height; } } } } for (int idx = 0; idx < span; idx++) { boolean proceed = true; if (span_width[idx] <= 0) continue; for (int colStart = col - (span - 1) + idx; colStart <= col + idx; colStart++) { if (colStart == col) continue; if (colWidth[colStart] <= 0) { proceed = false; break; } } if (proceed) { ret = span_width[idx]; for (int colStart = col - (span - 1) + idx; colStart <= col + idx; colStart++) { if (colStart == col) continue; ret -= colWidth[colStart]; } break; } } } return ret; } private static int[] calculateColumnWidth(TblCell[][] cellArray, int totalWidth) { int nWidth = cellArray[0].length; int[] colWidth = new int[nWidth]; // print for (int row = 0; row < cellArray.length; row++) { log.finest("CELLS [" + row + "]=" + Arrays.stream(cellArray[row]) .map(cell -> cell == null ? "" : String.valueOf(cell.width)).collect(Collectors.joining(","))); } // span loop for (int span = 1; span < nWidth; span++) { if (Arrays.stream(colWidth).filter(w -> w == 0).count() == 0) { log.finest("Span Loop:" + Arrays.stream(colWidth).mapToObj(w -> String.valueOf(w)).collect(Collectors.joining(","))); break; } // column loop for (int col = 0; col < nWidth; col++) { if (colWidth[col] != 0) continue; int nSpan = span; int nCol = col; List colWidthList = Arrays.stream(cellArray).flatMap(Arrays::stream) .filter(cell -> cell != null && cell.colSpan == nSpan && cell.colAddr <= nCol && cell.colAddr + nSpan >= nCol) .collect(Collectors.toList()); colWidth[col] = calculateEquasion(colWidthList, colWidth, col, span, true); } } // span loop for (int span = 2; span < nWidth; span++) { if (Arrays.stream(colWidth).filter(w -> w == 0).count() == 0) { log.finest("Span Loop:" + Arrays.stream(colWidth).mapToObj(w -> String.valueOf(w)).collect(Collectors.joining(","))); break; } // column loop for (int col = 0; col < nWidth; col++) { if (colWidth[col] != 0) continue; int nSpan = span; int nCol = col; List colWidthList = Arrays.stream(cellArray).flatMap(Arrays::stream) .filter(cell -> cell != null && cell.colSpan == nSpan && cell.colAddr <= nCol && cell.colAddr + nSpan >= nCol) .collect(Collectors.toList()); colWidth[col] = calculateEquasion(colWidthList, colWidth, col, span, true); } } log.finest("caculated:" + Arrays.stream(colWidth).mapToObj(w -> String.valueOf(w)).collect(Collectors.joining(","))); long emptyCols = Arrays.stream(colWidth).filter(w -> w <= 0).count(); long widthSum = Arrays.stream(colWidth).filter(w -> w > 0).sum(); if (emptyCols > 0) { int delta = (int) ((totalWidth - widthSum) / emptyCols); for (int col = 0; col < nWidth; col++) { if (colWidth[col] <= 0) { colWidth[col] = delta; } } } int[] reducedColWidth = IntStream.range(0, nWidth - 1).map(i -> colWidth[i]).toArray(); log.finest("reduced:" + Arrays.stream(reducedColWidth).mapToObj(w -> String.valueOf(w)).collect(Collectors.joining(","))); return reducedColWidth; } private static int[] calculateRowHeight(TblCell[][] cellArray, int totalHeight) { int nRow = cellArray.length; int[] rowHeight = new int[nRow]; if (nRow == 1) { rowHeight[0] = Arrays.stream(cellArray[0]).filter(c -> c!=null) .mapToInt(c -> c.height).min().orElse(0); } // print for (int row = 0; row < cellArray.length; row++) { log.finest("CELLS [" + row + "]=" + Arrays.stream(cellArray[row]) .map(cell -> cell == null ? "" : String.valueOf(cell.height)).collect(Collectors.joining(","))); } // span loop for (int span = 1; span < nRow; span++) { if (Arrays.stream(rowHeight).filter(w -> w == 0).count() == 0) { log.finest("Span Loop:" + Arrays.stream(rowHeight).mapToObj(w -> String.valueOf(w)).collect(Collectors.joining(","))); break; } // row loop for (int row = 0; row < nRow; row++) { if (rowHeight[row] != 0) continue; int nSpan = span; int nR = row; List rowHeightList = Arrays.stream(cellArray).flatMap(Arrays::stream) .filter(cell -> cell != null && cell.rowSpan == nSpan && cell.rowAddr <= nR && cell.rowAddr + nSpan >= nR) .collect(Collectors.toList()); rowHeight[row] = calculateEquasion(rowHeightList, rowHeight, row, span, false); } } // span loop for (int span = 2; span < nRow; span++) { if (Arrays.stream(rowHeight).filter(w -> w == 0).count() == 0) { log.finest("Span Loop:" + Arrays.stream(rowHeight).mapToObj(w -> String.valueOf(w)).collect(Collectors.joining(","))); break; } // row loop for (int row = 0; row < nRow; row++) { if (rowHeight[row] != 0) continue; int nSpan = span; int nR = row; List rowHeightList = Arrays.stream(cellArray).flatMap(Arrays::stream) .filter(cell -> cell != null && cell.rowSpan == nSpan && cell.rowAddr <= nR && cell.rowAddr + nSpan >= nR) .collect(Collectors.toList()); rowHeight[row] = calculateEquasion(rowHeightList, rowHeight, row, span, false); } } log.finest("caculated:" + Arrays.stream(rowHeight).mapToObj(w -> String.valueOf(w)).collect(Collectors.joining(","))); return rowHeight; } private static boolean hasNullRow(int[] rowHeight, int totalHeight) { long sum = Arrays.stream(rowHeight).sum(); if (Math.abs(totalHeight-sum) > totalHeight*5/100) return true; long cnt = Arrays.stream(rowHeight).filter(w -> w == 0).count(); if (cnt > 0) return true; else return false; } } H2Orestart-0.7.2/source/soffice/ConvUtil.java000066400000000000000000000633031476273367000210650ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package soffice; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.time.Duration; import java.time.LocalTime; import java.util.Arrays; import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Logger; import com.sun.star.awt.XBitmap; import com.sun.star.beans.Property; import com.sun.star.beans.PropertyValue; import com.sun.star.beans.UnknownPropertyException; import com.sun.star.beans.XPropertySet; import com.sun.star.beans.XPropertySetInfo; import com.sun.star.container.XNameAccess; import com.sun.star.lang.WrappedTargetException; import com.sun.star.lang.XMultiServiceFactory; import com.sun.star.text.XTextColumns; import com.sun.star.text.XTextRange; import com.sun.star.ucb.XFileIdentifierConverter; import com.sun.star.ucb.XSimpleFileAccess; import com.sun.star.uno.Any; import com.sun.star.uno.Exception; import com.sun.star.uno.XInterface; import com.sun.star.uno.UnoRuntime; import HwpDoc.paragraph.CharShape; public class ConvUtil { private static final Logger log = Logger.getLogger(ConvUtil.class.getName()); public static short selectCharShapeID(List charShapes, int startPos) { short charShapeID = 0; if (charShapes.size()==1) { charShapeID = (short)charShapes.get(0).charShapeID; } else if (charShapes.size()>1) { for(CharShape shape: charShapes) { if (startPos >= shape.start) { charShapeID = (short)shape.charShapeID; } } } return charShapeID; } public static String convertToURL(WriterContext wContext, String sBase, String sSystemPath) { String sURL = null; try { XFileIdentifierConverter xFileConverter = UnoRuntime.queryInterface(XFileIdentifierConverter.class, wContext.mMCF.createInstanceWithContext("com.sun.star.ucb.FileContentProvider", wContext.mContext)); sURL = xFileConverter.getFileURLFromSystemPath(sBase, sSystemPath ); } catch (com.sun.star.uno.Exception e) { e.printStackTrace(); } return sURL; } public static String convertToSystemPath(WriterContext wContext, String fileURL) { String systemPath = null; try { XFileIdentifierConverter xFileConverter = UnoRuntime.queryInterface(XFileIdentifierConverter.class, wContext.mMCF.createInstanceWithContext("com.sun.star.ucb.FileContentProvider", wContext.mContext)); systemPath = xFileConverter.getSystemPathFromFileURL(fileURL); } catch (com.sun.star.uno.Exception e) { e.printStackTrace(); } return systemPath; } public static boolean checkFile(WriterContext wContext, String aURL ){ boolean bExists = false; try { XSimpleFileAccess xSFA = UnoRuntime.queryInterface( XSimpleFileAccess.class, wContext.mMCF.createInstanceWithContext("com.sun.star.ucb.SimpleFileAccess", wContext.mContext)); bExists = xSFA.exists(aURL) && !xSFA.isFolder(aURL); } catch (com.sun.star.ucb.CommandAbortedException ex){ ex.printStackTrace(); } catch (com.sun.star.uno.Exception ex){ ex.printStackTrace(); } catch (java.lang.Exception ex){ ex.printStackTrace(); } return bExists; } public static String printRecursive(Class c, Object obj, int step) { StringBuffer sb = new StringBuffer("{"); try { while (c!=null) { List methods = Arrays.asList(c.getDeclaredMethods()); Field[] members = c.getDeclaredFields(); for (Field f: members) { // if (Modifier.isStatic(f.getModifiers()) && Modifier.isFinal(f.getModifiers())) continue; if (Modifier.isPrivate(f.getModifiers()) || Modifier.isProtected(f.getModifiers())) { if (f.getName().equals("m_value")) { Method getValueMethod = methods.stream().filter(m -> m.getName().equals("getValue")).findAny().get(); if (getValueMethod!=null) { Integer m_value = (Integer)getValueMethod.invoke(obj, null); sb.append("m_value="+m_value); } } } else { Object childObj = f.get(obj); String fieldName = f.getName(); Class childC = childObj.getClass(); if (fieldName.equals("UNOTYPEINFO")) continue; if (obj instanceof Any) { Method getObjectMethod = c.getDeclaredMethod("getObject", (Class[])null); Object internalObj = getObjectMethod.invoke(obj, (Object[])null); if (internalObj==null) { sb.append(fieldName+"=null"); } else { String value = printRecursive(childC, childObj, step+1); sb.append(fieldName+"="+value+","); } } else if (childC.getName().startsWith("java.lang")) { sb.append(fieldName+"="+childObj+","); } else if (childC.getName().startsWith("[")) { sb.append(fieldName+"="+printArrayRecursive(childC, childObj, step+1)+","); } else if (obj.getClass() != childObj.getClass()) { sb.append(fieldName+"="+printRecursive(childC, childObj, step+1)+","); } else { // enum type ??? // sb.append(fieldName+"="+childObj.toString()+","); } } } c = c.getSuperclass(); } } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException | NoSuchMethodException | SecurityException e) { e.printStackTrace(); } sb.append("}"); return sb.toString(); } public static String printArrayRecursive(Class c, Object obj, int step) { StringBuffer sb = new StringBuffer("["); try { int len = Array.getLength(obj); len = Math.min(len, 100); for (int i=0; i c = obj.getClass(); if (property.Name.equals("FillBitmapURL")) { log.finest("FillBitmapURL"); } switch(property.Type.getTypeName()) { case "boolean": case "short": case "long": case "string": case "byte": case "float": { Object propValue = xPropertySet.getPropertyValue(property.Name); if (propValue instanceof Any) { Any any = (Any)propValue; com.sun.star.uno.Type type = any.getType(); Object ob = any.getObject(); if (type.getTypeName().equals("com.sun.star.awt.XBitmap")) { XBitmap xBitmap = (XBitmap)UnoRuntime.queryInterface(XBitmap.class, ob); value = Base64.getEncoder().encodeToString(xBitmap.getDIB()); } else { value = ob==null?"":ob.toString(); } } else { value = propValue.toString(); } } break; case "com.sun.star.lang.Locale": { com.sun.star.lang.Locale locale = (com.sun.star.lang.Locale)xPropertySet.getPropertyValue(property.Name); value = locale.Country + "," + locale.Language; } break; case "com.sun.star.awt.Point": { com.sun.star.awt.Point point = (com.sun.star.awt.Point)xPropertySet.getPropertyValue(property.Name); value = point.X + "," + point.Y; } break; case "com.sun.star.awt.XBitmap": { Object ob = xPropertySet.getPropertyValue(property.Name); com.sun.star.awt.XBitmap xBitmap = (XBitmap) UnoRuntime.queryInterface(com.sun.star.awt.XBitmap.class, ob); value = Base64.getEncoder().encodeToString(xBitmap.getDIB()); } break; case "com.sun.star.awt.Rectangle": { com.sun.star.awt.Rectangle rect = (com.sun.star.awt.Rectangle)xPropertySet.getPropertyValue(property.Name); value = rect.X + "," + rect.Y + "," + rect.Width + "," + rect.Height; } break; case "com.sun.star.text.TextContentAnchorType": case "com.sun.star.drawing.BitmapMode": case "com.sun.star.drawing.RectanglePoint": case "com.sun.star.drawing.FillStyle": case "com.sun.star.drawing.LineStyle": case "com.sun.star.awt.FontSlant": case "com.sun.star.text.WrapTextMode": case "com.sun.star.text.WritingMode": case "com.sun.star.drawing.LineCap": case "com.sun.star.drawing.LineJoint": case "com.sun.star.drawing.TextAnimationDirection": case "com.sun.star.drawing.TextAnimationKind": case "com.sun.star.drawing.TextFitToSizeType": case "com.sun.star.drawing.TextHorizontalAdjust": case "com.sun.star.drawing.TextVerticalAdjust": { com.sun.star.uno.Enum unoEnum = (com.sun.star.uno.Enum)xPropertySet.getPropertyValue(property.Name); value = String.valueOf(unoEnum.getValue()); } break; case "com.sun.star.awt.Gradient": { com.sun.star.awt.Gradient gra = (com.sun.star.awt.Gradient)xPropertySet.getPropertyValue(property.Name); value = gra.StartColor + "," + gra.EndColor + "," + gra.Angle + "," + gra.Border + "," + gra.XOffset + "," + gra.YOffset + "," + gra.StartIntensity + "," + gra.EndIntensity + "," + gra.StepCount + "," + gra.Style.toString(); } break; case "com.sun.star.drawing.Hatch": { com.sun.star.drawing.Hatch hatch = (com.sun.star.drawing.Hatch)xPropertySet.getPropertyValue(property.Name); value = hatch.Color + "," + hatch.Distance + "," + hatch.Angle + "," + hatch.Style.toString(); } break; case "com.sun.star.text.GraphicCrop": { com.sun.star.text.GraphicCrop crop = (com.sun.star.text.GraphicCrop)xPropertySet.getPropertyValue(property.Name); value = crop.Top + "," + crop.Bottom + "," + crop.Left + "," + crop.Right; } break; case "com.sun.star.text.XTextColumns": { Object ob = xPropertySet.getPropertyValue(property.Name); XTextColumns xTC = (XTextColumns) UnoRuntime.queryInterface(XTextColumns.class, ob); value = xTC==null?"":String.valueOf(xTC.getColumnCount()); } break; case "com.sun.star.text.XTextRange": { Object ob = xPropertySet.getPropertyValue(property.Name); XTextRange xTR = (XTextRange) UnoRuntime.queryInterface(XTextRange.class, ob); value = xTR.getString(); } break; case "com.sun.star.drawing.LineDash": { com.sun.star.drawing.LineDash dash = (com.sun.star.drawing.LineDash)xPropertySet.getPropertyValue(property.Name); value = dash.Dots + "," + dash.DotLen + "," + dash.Dashes + "," + dash.DashLen + "," + dash.Distance; } break; case "com.sun.star.drawing.PolyPolygonBezierCoords": { com.sun.star.drawing.PolyPolygonBezierCoords poly = (com.sun.star.drawing.PolyPolygonBezierCoords)xPropertySet.getPropertyValue(property.Name); value = "" + poly.Coordinates.length + "[]"; } break; case "com.sun.star.style.LineSpacing": { com.sun.star.style.LineSpacing space = (com.sun.star.style.LineSpacing)xPropertySet.getPropertyValue(property.Name); value = space.Mode + "," + space.Height; } break; case "com.sun.star.text.XTextFrame": { Object ob = xPropertySet.getPropertyValue(property.Name); XInterface xInterface = (XInterface)UnoRuntime.queryInterface(property.Type.getClass(), ob); value = xInterface==null?"":xInterface.toString(); } break; case "com.sun.star.table.TableBorder": { com.sun.star.table.TableBorder border = (com.sun.star.table.TableBorder)xPropertySet.getPropertyValue(property.Name); value = "D=" + border.Distance; value += ",H=[" + border.HorizontalLine.Color + "," + border.HorizontalLine.InnerLineWidth + "," + border.HorizontalLine.LineDistance + "," + border.HorizontalLine.OuterLineWidth + "]"; value += ",V=[" + border.VerticalLine.Color + "," + border.VerticalLine.InnerLineWidth + "," + border.VerticalLine.LineDistance + "," + border.VerticalLine.OuterLineWidth + "]"; value += ",T=[" + border.TopLine.Color + "," + border.TopLine.InnerLineWidth + "," + border.TopLine.LineDistance + "," + border.TopLine.OuterLineWidth + "]"; value += ",L=["+border.LeftLine.Color+","+border.LeftLine.InnerLineWidth+","+border.LeftLine.LineDistance+","+border.LeftLine.OuterLineWidth+"]"; value += ",R=["+border.RightLine.Color+","+border.RightLine.InnerLineWidth+","+border.RightLine.LineDistance+","+border.RightLine.OuterLineWidth+"]"; value += ",B=[" + border.BottomLine.Color + "," + border.BottomLine.InnerLineWidth + "," + border.BottomLine.LineDistance + "," + border.BottomLine.OuterLineWidth + "]"; } break; case "com.sun.star.table.TableBorder2": { com.sun.star.table.TableBorder2 border = (com.sun.star.table.TableBorder2)xPropertySet.getPropertyValue(property.Name); value = "D=" + border.Distance; value += ",H=["+border.HorizontalLine.Color+","+border.HorizontalLine.InnerLineWidth+","+border.HorizontalLine.LineDistance+","+border.HorizontalLine.LineStyle+","+border.HorizontalLine.LineWidth+","+border.HorizontalLine.OuterLineWidth+"]"; value += ",V=["+border.VerticalLine.Color+","+border.VerticalLine.InnerLineWidth+","+border.VerticalLine.LineDistance+","+border.VerticalLine.LineStyle+","+border.VerticalLine.LineWidth+","+border.VerticalLine.OuterLineWidth+"]"; value += ",T=["+border.TopLine.Color+","+border.TopLine.InnerLineWidth+","+border.TopLine.LineDistance+","+border.TopLine.LineStyle+","+border.TopLine.LineWidth+","+border.TopLine.OuterLineWidth+"]"; value += ",L=["+border.LeftLine.Color+","+border.LeftLine.InnerLineWidth+","+border.LeftLine.LineDistance+","+border.LeftLine.LineStyle+","+border.LeftLine.LineWidth+","+border.LeftLine.OuterLineWidth+"]"; value += ",R=["+border.RightLine.Color+","+border.RightLine.InnerLineWidth+","+border.RightLine.LineDistance+","+border.RightLine.LineStyle+","+border.RightLine.LineWidth+","+border.RightLine.OuterLineWidth+"]"; value += ",B=["+border.BottomLine.Color+","+border.BottomLine.InnerLineWidth+","+border.BottomLine.LineDistance+","+border.BottomLine.LineStyle+","+border.BottomLine.LineWidth+","+border.BottomLine.OuterLineWidth+"]"; } break; case "com.sun.star.table.TableBorderDistances": { com.sun.star.table.TableBorderDistances dist = (com.sun.star.table.TableBorderDistances)xPropertySet.getPropertyValue(property.Name); value = dist.TopDistance + "," + dist.LeftDistance + "," + dist.RightDistance + "," + dist.BottomDistance; } break; case "com.sun.star.table.BorderLine": { com.sun.star.table.BorderLine borderLine = (com.sun.star.table.BorderLine)xPropertySet.getPropertyValue(property.Name); value = borderLine.Color + "," + borderLine.InnerLineWidth + "," + borderLine.OuterLineWidth + "," + borderLine.LineDistance; } break; case "com.sun.star.container.XIndexReplace": case "com.sun.star.container.XNameContainer": case "com.sun.star.drawing.HomogenMatrix3": case "com.sun.star.util.XComplexColor": case "com.sun.star.text.XTextSection": log.finest("unhandled type = " + property.Type.getTypeName()); break; default: if (property.Type.getTypeName().startsWith("[]")) { Object propValue = xPropertySet.getPropertyValue(property.Name); value = printArrayRecursive(c, propValue, step+1); } else { log.finest("unhandled type = " + property.Type.getTypeName()); Object propValue = xPropertySet.getPropertyValue(property.Name); value = "("+property.Type.getTypeName()+")"+printRecursive(c, propValue, step+1); } break; } log.finest(property.Name+"="+value); } catch (UnknownPropertyException | IllegalArgumentException | WrappedTargetException e) { e.printStackTrace(); } } } public static class DebuggingElapse { private static Map localTimeMap = new HashMap(); private static Map resultMap = new HashMap(); private static boolean enabled = true; static void setStart() { setStart("temp"); } static void setFinish() { setFinish("temp"); } static long getElapseTime() { return getElapseTime("temp"); } public static void setStart(String timerName) { localTimeMap.put(timerName, LocalTime.now()); } public static void setFinish(String timerName) { if (enabled) { LocalTime start = localTimeMap.get(timerName); localTimeMap.remove(timerName); if (start!=null) { LocalTime finish = LocalTime.now(); resultMap.put(timerName, Duration.between(start, finish).toMillis()); } } else { resultMap.put(timerName, 0L); } } public static long getElapseTime(String timerName) { if (enabled) { Long elapsed = resultMap.get(timerName); resultMap.remove(timerName); return elapsed==null?0:elapsed; } else { return 0; } } } /* get Version of LibreOffice * */ public static int getVersion(WriterContext wContext) { int version = 0; try { Object configProviderObject = wContext.mMCF.createInstanceWithContext("com.sun.star.configuration.ConfigurationProvider", wContext.mContext); XMultiServiceFactory xConfigServiceFactory = (XMultiServiceFactory) UnoRuntime.queryInterface(XMultiServiceFactory.class, configProviderObject); String readConfAccess = "com.sun.star.configuration.ConfigurationAccess"; PropertyValue[] properties = new PropertyValue[1]; properties[0] = new PropertyValue(); properties[0].Name = "nodepath"; properties[0].Value = "/org.openoffice.Setup/Product"; Object configReadAccessObject = xConfigServiceFactory.createInstanceWithArguments(readConfAccess, properties); XNameAccess xConfigNameAccess = (XNameAccess) UnoRuntime.queryInterface(XNameAccess.class, configReadAccessObject); String[] names = xConfigNameAccess.getElementNames(); for (String name: names) { String value = xConfigNameAccess.getByName(name).toString(); if (name.equals("ooSetupVersion")) { version = Integer.valueOf(value.replaceAll("(\\d)\\.(\\d)(\\.\\d)*", "$1$2")); // 6.4.1.2 -> 64 } // Name=ooName,Value=LibreOffice // Name=ooVendor,Value=The Document Foundation // Name=ooSetupVersion,Value=6.4 // Name=ooSetupExtension,Value=.7.2 // Name=ooSetupLastVersion,Value=7.1 // Name=LastTimeDonateShown,Value=1622522523 // Name=ooSetupVersionAboutBox,Value=6.4.7.2 // Name=LastTimeGetInvolvedShown,Value=1622522523 // Name=ooSetupVersionAboutBoxSuffix,Value= } } catch (Exception e) { e.printStackTrace(); log.severe(e.getMessage()); } return version; } } H2Orestart-0.7.2/source/soffice/HwpCallback.java000066400000000000000000000050431476273367000214720ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package soffice; import HwpDoc.paragraph.Ctrl_AutoNumber; public class HwpCallback { TableFrame tableFrame; public boolean firstParaAfterTable; public HwpCallback() { this.tableFrame = TableFrame.NONE; } public HwpCallback(TableFrame tableFrame) { this.tableFrame = tableFrame; } public void onNewNumber(int paraStyleID, int paraShapeID) {}; public void onAutoNumber(Ctrl_AutoNumber autoNumber, int paraStyleID, int paraShapeID) {}; public boolean onTab(String info) { return false; }; public boolean onText(String content, int charShapeId, int charPos, boolean append) { return false; } public boolean onParaBreak() { return false; } public TableFrame onTableWithFrame() { return tableFrame; } public void changeTableFrame(TableFrame tableFrame) { this.tableFrame = tableFrame; } public void onFirstAfterTable(boolean isFirst) { if (isFirst) { firstParaAfterTable = true; } else { firstParaAfterTable = false; } } public static enum TableFrame { NONE (0), NEVER (1), // 프레임 생성하지 말것. MADE (2), // 프레임 이미 생성되었음. MAKE (3), // 프레임 내부에서 만들것 MAKE_PART (4); // 프레임 내부에서 만들며, 내용을 반투명하게 만들것. private int num; private TableFrame(int num) { this.num = num; } public static TableFrame from(int num) { for (TableFrame type: values()) { if (type.num == num) return type; } return null; } } } H2Orestart-0.7.2/source/soffice/HwpRecurs.java000066400000000000000000000610421476273367000212420ustar00rootroot00000000000000/* Copyright (C) 2023 ebandal * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package soffice; import java.util.ListIterator; import java.util.logging.Logger; import java.util.stream.Collectors; import com.sun.star.beans.XPropertySet; import com.sun.star.text.ControlCharacter; import com.sun.star.text.XParagraphCursor; import com.sun.star.text.XTextCursor; import com.sun.star.text.XTextRange; import com.sun.star.uno.UnoRuntime; import HwpDoc.HwpElement.HwpRecord_BorderFill; import HwpDoc.HwpElement.HwpRecord_CharShape; import HwpDoc.HwpElement.HwpRecord_ParaShape; import HwpDoc.HwpElement.HwpRecord_Style; import HwpDoc.paragraph.Ctrl; import HwpDoc.paragraph.Ctrl_AutoNumber; import HwpDoc.paragraph.Ctrl_Character; import HwpDoc.paragraph.Ctrl_ColumnDef; import HwpDoc.paragraph.Ctrl_Common; import HwpDoc.paragraph.Ctrl_EqEdit; import HwpDoc.paragraph.Ctrl_GeneralShape; import HwpDoc.paragraph.Ctrl_HeadFoot; import HwpDoc.paragraph.Ctrl_NewNumber; import HwpDoc.paragraph.Ctrl_Note; import HwpDoc.paragraph.Ctrl_SectionDef; import HwpDoc.paragraph.Ctrl_Table; import HwpDoc.paragraph.ParaText; import HwpDoc.section.Page; import soffice.HwpCallback.TableFrame; import HwpDoc.paragraph.HwpParagraph; import HwpDoc.paragraph.LineSeg; public class HwpRecurs { private static final Logger log = Logger.getLogger(HwpRecurs.class.getName()); private static short oldParaShapeID; private static short oldCharShapeID; private static final String PATTERN_STRING = "[\\u0000\\u000a\\u000d\\u0018-\\u001f]|[\\u0001\\u0002-\\u0009\\u000b-\\u000c\\u000e-\\u0017].{6}[\\u0001\\u0002-\\u0009\\u000b-\\u000c\\u000e-\\u0017]"; // 컨트롤에 쓰기는 wContext로, 페이지에 쓰기는 parentWriterContext로 (각주,미주) public static void printParaRecurs(WriterContext wContext, WriterContext parentWriterContext, HwpParagraph para, HwpCallback callback, int step) { // PARA_BREAK 후 return 되기 전에 default로 만들 필요 있음. 그래서 가장 먼저 한다. if (step<=1 && oldParaShapeID!=para.paraShapeID) { ConvPara.setDefaultParaStyle(wContext); } if (para.breakType > 0) { if ((para.breakType&0x01)==0x01) { // 구역나누기 ConvPage.makeNextPage(wContext); } else if ((para.breakType&0x02)==0x02) { // 다단나누기 } else if ((para.breakType&0x04)==0x04) { // 쪽 나누기 ConvPage.makeNextPage(wContext); // 쪽 변경시 (SECD없이도) 페이지 가로,세로가 바뀌는 경우 있음. 국방CBD방법론v2(2권).hwp // 쪽만 바뀌면서 HEADER가 중복으로 추가되어 표현됨. 디버깅 할 것. 머리글서식을 하나씩 추가해야 할것 같음. } else if ((para.breakType&0x08)==0x08) { // 단 나누기 ConvPage.makeNextColumn(wContext); } } if (para.p==null) { if (callback==null || callback.onParaBreak()==false) { beforeParaBreak(wContext, para.paraShapeID, oldCharShapeID, false, step); wContext.mText.insertControlCharacter(wContext.mTextCursor, ControlCharacter.PARAGRAPH_BREAK, false); } return; } // Start of [Overcome Table discrepancy] // HWP table과 LibreOffice table 표현 방법이 다름을 극복하기 위한 방법 // 다른 공통개체(그림으로 표시하는 개체)가 현재 문단내에 존재하는지 갯수를 가져온다. 단, 문자로 취급하지 않는 개체만 카운트한다. long objCount = para.p.stream().filter(c -> ((c instanceof Ctrl_Common) && ((Ctrl_Common)c).treatAsChar==false) || ((c instanceof Ctrl_Table) && ((Ctrl_Common)c).treatAsChar==true) ) .collect(Collectors.counting()); // 글자가 포함되어 있는지 가져온다. String remainChars = para.p.stream().filter(c -> (c instanceof ParaText)) .map(c -> (ParaText)c) .map(t -> t.text.replaceAll(PATTERN_STRING, "")).collect(Collectors.joining()); boolean oweParaBreak = false; // End of [Overcome Table discrepancy] // Start of [Overcome COLD > SECD order] boolean secdDone = false; // End of [Overcome COLD > SECD order] boolean append = false; for (int ctrlIndex=0; ctrlIndex charShapeList = para.charShapes.stream().filter(s -> s.start>=startIndex).collect(Collectors.toList()); // CharShape[] charShapes = charShapeList.toArray(new CharShape[charShapeList.size()]); int charShapeId = ((ParaText)ctrl).charShapeId; if (callback==null || callback.onText(((ParaText)ctrl).text, charShapeId, startIndex, append)==false) { insertParaString(wContext, ((ParaText)ctrl).text, para.lineSegs, para.paraStyleID, para.paraShapeID, (short)charShapeId, append, callback==null?false:callback.firstParaAfterTable, step); oldParaShapeID = para.paraShapeID; oldCharShapeID = (short) charShapeId; } append = true; } break; case " _": { switch(((Ctrl_Character)ctrl).ctrlChar) { case LINE_BREAK: wContext.mText.insertControlCharacter(wContext.mTextCursor, ControlCharacter.LINE_BREAK, false); break; case PARAGRAPH_BREAK: if (callback==null || oweParaBreak==false) { if (callback==null || callback.onParaBreak()==false) { beforeParaBreak(wContext, para.paraShapeID, (short)((Ctrl_Character)ctrl).charShapeId, false, step); wContext.mText.insertControlCharacter(wContext.mTextCursor, ControlCharacter.PARAGRAPH_BREAK, false); } if (callback!=null) { callback.onFirstAfterTable(false); } } else { oweParaBreak = false; if (callback!=null) { callback.onFirstAfterTable(true); } } break; case HARD_HYPHEN: wContext.mText.insertControlCharacter(wContext.mTextCursor, ControlCharacter.HARD_HYPHEN, false); break; case HARD_SPACE: wContext.mText.insertControlCharacter(wContext.mTextCursor, ControlCharacter.HARD_SPACE, false); break; } } break; case "dces": if (step==1) { // 1depth에서만 처리 if (secdDone==false) { ConvPage.setupPage(wContext, ((Ctrl_SectionDef)ctrl).page); secdDone = true; } } break; case "dloc": if (step==1) { // 1depth에서만 처리 if (secdDone == false) { Ctrl_SectionDef ctrlSecd = para.p.stream().filter(c -> (c instanceof Ctrl_SectionDef)) .map(c -> (Ctrl_SectionDef)c).findAny().orElse(null); if (ctrlSecd!=null) { ConvPage.setupPage(wContext, ctrlSecd.page); secdDone = true; } } ConvPage.setColumn(wContext, (Ctrl_ColumnDef)ctrl); } break; case "daeh": // 머리말 case "toof": // 꼬리말 ConvPage.setHeaderFooter(wContext, (Ctrl_HeadFoot)ctrl); break; case " nf": // 각주 case " ne": // 미주 // 미주,각주는 상위 WriterContext로 출력 ConvFootnote.insertFootnote(parentWriterContext, (Ctrl_Note) ctrl, step+1); break; case " lbt": // table { if (callback!=null && callback.firstParaAfterTable==true && ((Ctrl_Table)ctrl).treatAsChar==true) { beforeParaBreak(wContext, null, null, false, true, step); wContext.mText.insertControlCharacter(wContext.mTextCursor, ControlCharacter.PARAGRAPH_BREAK, false); } // 이전에 table 이고, 이번에도 table이면 공백을 추가한다. else if (ctrlIndex > 0) { Ctrl previous = para.p.get(ctrlIndex-1); if (previous instanceof Ctrl_Table && ((Ctrl_Table)previous).treatAsChar==true) { wContext.mText.insertControlCharacter(wContext.mTextCursor, ControlCharacter.HARD_SPACE, false); } } // 테이블을 그릴때는 문단에 한개의 테이블만 있는지(table + split속성), 다른 개체나 문장과 같이 있는지(table in textframe)에 따라 다르게 그린다. boolean hasSibling = objCount>1?true:remainChars.length()>1?true:false; // 문단내에 1개 테이블외 다른것이 포함되어 있는지 나타냄 TableFrame oldFrame = callback==null?TableFrame.NONE:callback.tableFrame; if (hasSibling==true) { if (callback==null) { callback = new HwpCallback(); } if (callback!=null && callback.onTableWithFrame()!=TableFrame.MADE) { // (table이 여러 페이지에 걸쳐서 있는지 체크하기 위해) row 높이를 모두 더해서 페이지보다 큰지 체크한다. Ctrl_Table table = (Ctrl_Table)ctrl; int rowHeightSum = 0; int pageHeight = 0; if (table.rowSize != null) { for (int row=0, cellIndex=0; row */ /* 본 제품은 한글과컴퓨터의 ᄒᆞᆫ글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. * 개방형 워드프로세서 마크업 언어(OWPML) 문서 구조 KS X 6101:2018 문서를 참고하였습니다. * 작성자 : 반희수 ebandal@gmail.com * 작성일 : 2022.10 */ package soffice; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.logging.Logger; import java.util.zip.DataFormatException; import javax.xml.parsers.ParserConfigurationException; import org.xml.sax.SAXException; import com.sun.star.frame.XDesktop; import com.sun.star.lang.XMultiComponentFactory; import com.sun.star.lang.XMultiServiceFactory; import com.sun.star.text.XText; import com.sun.star.text.XTextCursor; import com.sun.star.text.XTextDocument; import com.sun.star.uno.XComponentContext; import HwpDoc.HanType; import HwpDoc.HwpDetectException; import HwpDoc.HwpDocInfo; import HwpDoc.HwpFile; import HwpDoc.HwpSection; import HwpDoc.HwpxFile; import HwpDoc.Exception.CompoundDetectException; import HwpDoc.Exception.CompoundParseException; import HwpDoc.Exception.HwpParseException; import HwpDoc.Exception.NotImplementedException; import HwpDoc.Exception.OwpmlParseException; import HwpDoc.HwpElement.HwpRecord_BinData; import HwpDoc.HwpElement.HwpRecord_BorderFill; import HwpDoc.HwpElement.HwpRecord_Bullet; import HwpDoc.HwpElement.HwpRecord_CharShape; import HwpDoc.HwpElement.HwpRecord_Numbering; import HwpDoc.HwpElement.HwpRecord_ParaShape; import HwpDoc.HwpElement.HwpRecord_Style; import HwpDoc.HwpElement.HwpRecord_TabDef; import HwpDoc.HwpElement.HwpRecord_BinData.Type; public class WriterContext { private static final Logger log = Logger.getLogger(WriterContext.class.getName()); private static HanType hType; public static HwpFile hwp = null; public static HwpxFile hwpx = null; public static int version; public static Set fontNameSet = new HashSet(); public XDesktop mDesktop = null; public XComponentContext mContext = null; public XMultiComponentFactory mMCF = null; public XMultiServiceFactory mMSF = null; public XTextDocument mMyDocument = null; public XText mText = null; public XTextCursor mTextCursor = null; public Path userHomeDir = null; public WriterContext() { } public List getSections() throws HwpDetectException { List sections = null; switch (hType) { case HWP: sections = hwp.getSections(); break; case HWPX: sections = hwpx.getSections(); break; case NONE: throw new HwpDetectException(); } return sections; } public static String detectHancom(File file) { String detectingType = null; try { HwpxFile hwpxTemp = new HwpxFile(file); hwpxTemp.detect(); detectingType = "HWPX"; hwpxTemp.close(); log.info("file detected as HWPX"); } catch (IOException | HwpDetectException e1) { log.info("file detected not HWPX"); try { HwpFile hwpTemp = new HwpFile(file); hwpTemp.detect(); detectingType = "HWP"; hwpTemp.close(); log.info("file detected as HWP"); } catch (IOException | HwpDetectException e2) { log.info("file detected neither HWPX nor HWP"); } } return detectingType; } public void detect() throws HwpDetectException, CompoundDetectException, NotImplementedException, IOException, CompoundParseException, ParserConfigurationException, SAXException, DataFormatException { switch (hType) { case HWP: hwp.detect(); break; case HWPX: hwpx.detect(); break; case NONE: throw new HwpDetectException(); } } public void open(String inputFile, String hanTypeStr) throws HwpDetectException, CompoundDetectException, IOException, DataFormatException, HwpParseException, NotImplementedException, CompoundParseException, ParserConfigurationException, SAXException, OwpmlParseException { switch (hanTypeStr) { case "HWP": hType = HanType.HWP; hwp = new HwpFile(inputFile); hwp.open(); break; case "HWPX": hType = HanType.HWPX; hwpx = new HwpxFile(inputFile); hwpx.open(); break; default: throw new HwpDetectException(); } } public void open(File inputFile, String hanTypeStr) throws HwpDetectException, CompoundDetectException, IOException, DataFormatException, HwpParseException, NotImplementedException, CompoundParseException, ParserConfigurationException, SAXException, OwpmlParseException { switch (hanTypeStr) { case "HWP": hType = HanType.HWP; hwp = new HwpFile(inputFile); hwp.open(); break; case "HWPX": hType = HanType.HWPX; hwpx = new HwpxFile(inputFile); hwpx.open(); break; default: throw new HwpDetectException(); } } public void close() throws IOException, HwpDetectException { if (hType != null) { switch (hType) { case HWP: hwp.close(); break; case HWPX: hwpx.close(); break; case NONE: throw new HwpDetectException(); } } fontNameSet.clear(); } public HwpDocInfo getDocInfo() { HwpDocInfo docInfo = null; switch (hType) { case HWP: docInfo = hwp.getDocInfo(); break; case HWPX: docInfo = hwpx.getDocInfo(); break; } return docInfo; } public static HwpRecord_BorderFill getBorderFill(short id) { HwpDocInfo docInfo = null; switch (hType) { case HWP: docInfo = hwp.getDocInfo(); break; case HWPX: docInfo = hwpx.getDocInfo(); break; } return (id > 0 ? (HwpRecord_BorderFill) docInfo.borderFillList.get(id - 1) : null); } public HwpRecord_ParaShape getParaShape(int id) { HwpDocInfo docInfo = null; switch (hType) { case HWP: docInfo = hwp.getDocInfo(); break; case HWPX: docInfo = hwpx.getDocInfo(); break; } if (id >= 0 && id < docInfo.paraShapeList.size()) { return (HwpRecord_ParaShape) docInfo.paraShapeList.get(id); } else { return null; } } public HwpRecord_Style getParaStyle(short id) { HwpDocInfo docInfo = null; switch (hType) { case HWP: docInfo = hwp.getDocInfo(); break; case HWPX: docInfo = hwpx.getDocInfo(); break; } return (HwpRecord_Style) docInfo.styleList.get(id); } public HwpRecord_CharShape getCharShape(int id) { HwpDocInfo docInfo = null; switch (hType) { case HWP: docInfo = hwp.getDocInfo(); break; case HWPX: docInfo = hwpx.getDocInfo(); break; } if (id >= 0 && id < docInfo.charShapeList.size()) { return (HwpRecord_CharShape) docInfo.charShapeList.get(id); } else { return null; } } public HwpRecord_Numbering getNumbering(short id) { HwpDocInfo docInfo = null; switch (hType) { case HWP: docInfo = hwp.getDocInfo(); break; case HWPX: docInfo = hwpx.getDocInfo(); break; } return (HwpRecord_Numbering) docInfo.numberingList.get(id); } public HwpRecord_Bullet getBullet(short id) { HwpDocInfo docInfo = null; switch (hType) { case HWP: docInfo = hwp.getDocInfo(); break; case HWPX: docInfo = hwpx.getDocInfo(); break; } return (HwpRecord_Bullet) docInfo.bulletList.get(id - 1); } public String getBinFilename(String id) { HwpRecord_BinData binData = null; String retString = ""; HwpDocInfo docInfo = null; switch (hType) { case HWP: docInfo = hwp.getDocInfo(); ArrayList keyList = new ArrayList(docInfo.binDataList.keySet()); String key = keyList.get(Integer.parseInt(id)); binData = (HwpRecord_BinData) docInfo.binDataList.get(key); break; case HWPX: docInfo = hwpx.getDocInfo(); binData = (HwpRecord_BinData) docInfo.binDataList.get(id); break; } retString = binData.aPath; /* * String compoundFileName = String.format("BIN%04X.%s", binData.binDataID, * binData.format); try { retString = hwp.saveChildEntry(getWorkingFolder(), * compoundFileName, binData.compressed); } catch (IOException e) { * e.printStackTrace(); } */ return retString; } public byte[] getBinBytes(String id) { byte[] imageBytes = null; HwpDocInfo docInfo = null; switch (hType) { case HWP: { docInfo = hwp.getDocInfo(); ArrayList keyList = new ArrayList(docInfo.binDataList.keySet()); String key = keyList.get(Integer.parseInt(id)); HwpRecord_BinData binData = (HwpRecord_BinData) docInfo.binDataList.get(key); if (binData != null) { if (binData.type == Type.LINK) { File file = new File(binData.aPath); try { imageBytes = Files.readAllBytes(file.toPath()); } catch (IOException e) { e.printStackTrace(); } } else { String compoundFileName = String.format("BIN%04X.%s", binData.binDataID, binData.format); try { imageBytes = hwp.getChildBytes(compoundFileName, binData.compressed); } catch (IOException e) { e.printStackTrace(); } } } } break; case HWPX: { docInfo = hwpx.getDocInfo(); HwpRecord_BinData binData = (HwpRecord_BinData) docInfo.binDataList.get(id); if (binData != null) { try { String binShortName = binData.aPath.replaceAll("BinData/(.*)\\..*", "$1"); imageBytes = hwpx.getBinDataByIDRef(binShortName); } catch (IOException | DataFormatException e) { e.printStackTrace(); } } } break; } return imageBytes; } public String getBinFormat(String id) { HwpRecord_BinData binData = null; HwpDocInfo docInfo = null; switch (hType) { case HWP: { docInfo = hwp.getDocInfo(); ArrayList keyList = new ArrayList(docInfo.binDataList.keySet()); String key = keyList.get(Integer.parseInt(id)); binData = (HwpRecord_BinData) docInfo.binDataList.get(key); } break; case HWPX: { docInfo = hwpx.getDocInfo(); binData = (HwpRecord_BinData) docInfo.binDataList.get(id); } break; } return binData.format; } public static HwpRecord_TabDef getTabDef(short id) { HwpDocInfo docInfo = null; switch (hType) { case HWP: docInfo = hwp.getDocInfo(); break; case HWPX: docInfo = hwpx.getDocInfo(); break; } return (HwpRecord_TabDef) docInfo.tabDefList.get(id); } public static HwpxFile getHwpx() { return hwpx; } }