commit dc5538dd68721517d764da64f9dcfcd8dd8bcccc Author: jan Date: Mon Sep 18 03:01:48 2017 -0700 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..715503a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +__pycache__ +*.idea diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..4ef32f0 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,651 @@ +GNU Affero General Public License +================================= + +_Version 3, 19 November 2007_ +_Copyright © 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 Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + +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. + +Developers that use our General Public Licenses protect your rights +with two steps: **(1)** assert copyright on the software, and **(2)** offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + +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 Affero 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. Remote Network Interaction; Use with the GNU General Public License + +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + +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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + +### 14. Revised Versions of this License + +The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a “Source” link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + +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 AGPL, see +<>. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f4fd9be --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# fatamorgana + +**fatamorgana** is a Python package for reading and writing OASIS format layout files. + + +**Capabilities:** +* This package is a work-in-progress and is largely untested -- it works for + the tasks I usually use it for, but I can't guarantee I've even + tried the features you happen to use! Use at your own risk! +* Interfaces and datastructures are subject to change! +* That said the following work for me: + - polygons + - layer info + - cell names + - compressed blocks + - basic property I/O + + +## Installation + +**Dependencies:** +* python 3.5 or newer +* (optional) numpy + + +Install with pip from PyPi: +```bash +pip install fatamorgana +``` + +Install directly from git repository: +```bash +pip install git+https://mpxd.net/gogs/jan/fatamorgana.git@release +``` + +## Use + + diff --git a/fatamorgana/__init__.py b/fatamorgana/__init__.py new file mode 100644 index 0000000..215175a --- /dev/null +++ b/fatamorgana/__init__.py @@ -0,0 +1,26 @@ +""" + fatamorgana + + fatamorgana is a python package for reading and writing to the + OASIS layout format. The OASIS format ('.oas') is the successor to + GDSII ('.gds') and boasts + - Additional primitive shapes + - Arbitrary-length integers and fractions + - Extra ways to represent arrays of repeated shapes + - Better support for arbitrary ASCII text data + - More compact data storage format + - Inline compression + + fatamorana is written in pure python and only optionally depends on + numpy to speed up reading/writing. + + Dependencies: + - Python 3.5 or later + - numpy (optional, no additional functionality) +""" +from .main import OasisLayout, Cell, XName +from .basic import NString, AString, Validation, OffsetTable, OffsetEntry, \ + EOFError, SignedError, InvalidDataError, InvalidRecordError + + +__author__ = 'Jan Petykiewicz' diff --git a/fatamorgana/basic.py b/fatamorgana/basic.py new file mode 100644 index 0000000..5a45b38 --- /dev/null +++ b/fatamorgana/basic.py @@ -0,0 +1,1922 @@ +""" +This module contains all datatypes and parsing/writing functions for + all abstractions below the 'record' or 'block' level. +""" +from fractions import Fraction +from typing import List, Tuple, Type +from enum import Enum +import math +import struct +import io + +try: + import numpy + _USE_NUMPY = True +except: + _USE_NUMPY = False + + +''' + Type definitions +''' +real_t = int or float or Fraction +repetition_t = 'ReuseRepetition' or 'GridRepetition' or 'ArbitraryRepetition' +property_value_t = int or bytes or 'AString' or 'NString' or' PropStringReference' or float or Fraction + + +class EOFError(Exception): + pass + + +class SignedError(Exception): + pass + + +class InvalidDataError(Exception): + pass + + +class InvalidRecordError(Exception): + pass + + +class PathExtensionScheme(Enum): + """ + Enum for path extension schemes + """ + Flush = 1 + HalfWidth = 2 + Arbitrary = 3 + + + +''' + Constants +''' +MAGIC_BYTES = b'%SEMI-OASIS\r\n' # type: bytes + + +''' + Basic IO +''' +def _read(stream: io.BufferedIOBase, n: int) -> bytes: + """ + Read n bytes from the stream. + Raise an EOFError if there were not enough bytes in the stream. + + :param stream: Stream to read from. + :param n: Number of bytes to read. + :return: The bytes that were read. + :raises: EOFError if not enough bytes could be read. + """ + b = stream.read(n) + if len(b) != n: + raise EOFError('Unexpected EOF') + return b + + +def read_byte(stream: io.BufferedIOBase) -> int: + """ + Read a single byte and return it. + + :param stream: Stream to read from. + :return: The byte that was read. + """ + return _read(stream, 1)[0] + + +def write_byte(stream: io.BufferedIOBase, n: int) -> int: + """ + Write a single byte to the stream. + + :param stream: Stream to read from. + :return: The number of bytes writen (1). + """ + return stream.write(bytes((n,))) + + +if _USE_NUMPY: + def read_bool_byte(stream: io.BufferedIOBase) -> List[bool]: + """ + Read a single byte from the stream, and interpret its bits as + a list of 8 booleans. + + :param stream: Stream to read from. + :return: A list of 8 booleans corresponding to the bits (MSB first). + """ + byte_arr = _read(stream, 1) + return numpy.unpackbits(numpy.frombuffer(byte_arr, dtype=numpy.uint8)) + + def write_bool_byte(stream: io.BufferedIOBase, bits: Tuple[bool]) -> int: + """ + Pack 8 booleans into a byte, and write it to the stream. + + :param stream: Stream to write to. + :param bits: A list of 8 booleans corresponding to the bits (MSB first). + :return: Number of bytes written (1). + :raises: InvalidDataError if didn't receive 8 bits. + """ + if len(bits) != 8: + raise InvalidDataError('write_bool_byte received {} bits, requires 8'.format(len(bits))) + return stream.write(numpy.packbits(bits)) +else: + def read_bool_byte(stream: io.BufferedIOBase) -> List[bool]: + """ + Read a single byte from the stream, and interpret its bits as + a list of 8 booleans. + + :param stream: Stream to read from. + :return: A list of 8 booleans corresponding to the bits (MSB first). + """ + byte = _read(1)[0] + bits = [(byte >> i) & 0x01 for i in reversed(range(8))] + return bits + + def write_bool_byte(stream: io.BufferedIOBase, bits: Tuple[bool]) -> int: + """ + Pack 8 booleans into a byte, and write it to the stream. + + :param stream: Stream to write to. + :param bits: A list of 8 booleans corresponding to the bits (MSB first). + :return: Number of bytes written (1). + :raises: InvalidDataError if didn't receive 8 bits. + """ + if len(bits) != 8: + raise InvalidDataError('write_bool_byte received {} bits, requires 8'.format(len(bits))) + byte = 0 + for i, bit in enumerate(reversed(bits)): + byte |= bit << i + return stream.write(bytes((byte))) + + +def read_uint(stream: io.BufferedIOBase) -> int: + """ + Read an unsigned integer from the stream. + + The format used is sometimes called a "varint": + - MSB of each byte is set to 1, except for the final byte. + - Remaining bits of each byte form the binary representation + of the integer, but are stored _least significant group first_. + + :param stream: Stream to read from. + :return: The integer's value. + """ + result = 0 + i = 0 + byte = _read(stream, 1)[0] + result |= byte & 0x7f + while byte & 0x80: + i += 1 + byte = _read(stream, 1)[0] + result |= (byte & 0x7f) << (7 * i) + return result + + +def write_uint(stream: io.BufferedIOBase, n: int) -> int: + """ + Write an unsigned integer to the stream. + See format details in read_uint(...). + + :param stream: Stream to write to. + :param n: Value to write. + :return: The number of bytes written. + :raises: SignedError if n is negative. + """ + if n < 0: + raise SignedError('uint must be positive: {}'.format(n)) + + current = n + byte_list = [] + while True: + byte = current & 0x7f + current >>= 7 + if current != 0: + byte |= 0x80 + byte_list.append(byte) + else: + byte_list.append(byte) + break + return stream.write(bytes(byte_list)) + + +def decode_sint(uint: int) -> int: + """ + Decode a signed integer from its unsigned form. + + The encoded form is sometimes called "zigzag" representation: + - The LSB is treated as the sign bit + - The remainder of the bits encodes the absolute value + + :param uint: Unsigned integer to decode from. + :return: The decoded signed integer. + """ + return (uint >> 1) * (1 - 2 * (0x01 & uint)) + + +def encode_sint(sint: int) -> int: + """ + Encode a signed integer into its corresponding unsigned integer form. + See decode_sint() for format details. + + :param int: The signed integer to encode. + :return: Unsigned integer encoding for the input. + """ + return (abs(sint) << 1) | (sint < 0) + + +def read_sint(stream: io.BufferedIOBase) -> int: + """ + Read a signed integer from the stream. + See decode_sint() for format details. + + :param stream: Stream to read from. + :return: The integer's value. + """ + return decode_sint(read_uint(stream)) + + +def write_sint(stream: io.BufferedIOBase, n: int) -> int: + """ + Write a signed integer to the stream. + See decode_sint() for format details. + + :param stream: Stream to write to. + :param n: Value to write. + :return: The number of bytes written. + """ + return write_uint(stream, encode_sint(n)) + + +def read_bstring(stream: io.BufferedIOBase) -> bytes: + """ + Read a binary string from the stream. + The format is: + - length: uint + - data: bytes + + :param stream: Stream to read from. + :return: Bytes containing the binary string. + """ + length = read_uint(stream) + return _read(stream, length) + + +def write_bstring(stream: io.BufferedIOBase, bstring: bytes): + """ + Write a binary string to the stream. + See read_bstring() for format details. + + :param stream: Stream to write to. + :param bstring: Binary string to write. + :return: The number of bytes written. + """ + write_uint(stream, len(bstring)) + return stream.write(bstring) + + +def read_ratio(stream: io.BufferedIOBase) -> Fraction: + """ + Read a ratio (unsigned) from the stream. + The format is: + - numerator: uint + - denominator: uint + + :param stream: Stream to read from. + :return: Fraction object containing the read value. + """ + numer = read_uint(stream) + denom = read_uint(stream) + return Fraction(numer, denom) + + +def write_ratio(stream: io.BufferedIOBase, r: Fraction) -> int: + """ + Write an unsigned ratio to the stream. + See read_ratio() for format details. + + :param stream: Stream to write to. + :param r: Ratio to write (Fraction object). + :return: The number of bytes written. + :raises: SignedError if r is negative. + """ + if r < 0: + raise SignedError('Ratio must be unsigned: {}'.format(r)) + size = write_uint(stream, r.numerator) + size += write_uint(stream, r.denominator) + return size + + +def read_float32(stream: io.BufferedIOBase) -> float: + """ + Read a 32-bit float from the stream. + + :param stream: Stream to read from. + :return: The value read. + """ + b = _read(stream, 4) + return struct.unpack(" int: + """ + Write a 32-bit float to the stream. + + :param stream: Stream to write to. + :param f: Value to write. + :return: The number of bytes written (4). + """ + b = struct.pack(" float: + """ + Read a 64-bit float from the stream. + + :param stream: Stream to read from. + :return: The value read. + """ + b = _read(stream, 8) + return struct.unpack(" int: + """ + Write a 64-bit float to the stream. + + :param stream: Stream to write to. + :param f: Value to write. + :return: The number of bytes written (8). + """ + b = struct.pack(" real_t: + """ + Read a real number from the stream. + + Format consists of a uint denoting the type, which can be passed + as an argument or read from the stream (default), followed by the + type-dependent value: + + 0: uint (positive) + 1: uint (negative) + 2: uint (positive reciprocal, i.e. 1/u) + 3: uint (negative reciprocal, i.e. -1/u) + 4: ratio (positive) + 5: ratio (negative) + 6: 32-bit float + 7: 64-bit float + + :param stream: Stream to read from. + :param real_type: Type of real number to read. If None (default), + the type is read from the stream. + :return: The value read. + :raises: InvalidDataError if real_type is invalid. + """ + + if real_type is None: + real_type = read_uint(stream) + if real_type == 0: + return read_uint(stream) + if real_type == 1: + return -read_uint(stream) + if real_type == 2: + return Fraction(1, read_uint(stream)) + if real_type == 3: + return Fraction(-1, read_uint(stream)) + if real_type == 4: + return Fraction(read_uint(stream), read_uint(stream)) + if real_type == 5: + return Fraction(-read_uint(stream), read_uint(stream)) + if real_type == 6: + return read_float32(stream) + if real_type == 7: + return read_float64(stream) + raise InvalidDataError('Invalid real type: {}'.format(real_type)) + + +def write_real(stream: io.BufferedIOBase, + r: real_t, + force_float32: bool = False + ) -> int: + """ + Write a real number to the stream. + See read_real() for format details. + + This function will store r as an int if it is already an int, + but will not cast it into an int if it is an integer-valued + float or Fraction. + Since python has no 32-bit floats, the force_float32 parameter + will perform the cast at write-time if set to True (default False). + + :param stream: Stream to write to. + :param r: Value to write. + :param float32: + :return: The number of bytes written. + """ + size = 0 + if isinstance(r, int): + size += write_uint(stream, r < 0) + size += write_uint(stream, abs(r)) + elif isinstance(r, Fraction): + if abs(r.numerator) == 1: + size += write_uint(stream, 2 + (r < 0)) + size += write_uint(stream, abs(r.denominator)) + else: + size += write_uint(stream, 4 + (r < 0)) + size += write_ratio(stream, abs(r)) + elif isinstance(r, float): + if force_float32: + size += write_uint(stream, 6) + size += write_float32(stream, r) + else: + size += write_uint(stream, 7) + size += write_float64(stream, r) + return size + + +class NString: + """ + Class for handling "name strings", which hold one or more + printable ASCII characters (0x21 to 0x7e, inclusive). + + __init__ can be called with either a string or bytes object; + subsequent reading/writing should use the .string and + .bytes properties. + """ + _string = None # type: str + + def __init__(self, string_or_bytes: bytes or str): + """ + :param string_or_bytes: Content of the Nstring. + """ + if isinstance(string_or_bytes, str): + self.string = string_or_bytes + else: + self.bytes = string_or_bytes + + @property + def string(self) -> str: + return self._string + + @string.setter + def string(self, string: str): + if len(string) == 0 or not all(0x21 <= ord(c) <= 0x7e for c in string): + raise InvalidDataError('Invalid n-string {}'.format(string)) + self._string = string + + @property + def bytes(self) -> bytes: + return self._string.encode('ascii') + + @bytes.setter + def bytes(self, bstring: bytes): + if len(bstring) == 0 or not all(0x21 <= c <= 0x7e for c in bstring): + raise InvalidDataError('Invalid n-string {}'.format(bstring)) + self._string = bstring.decode('ascii') + + @staticmethod + def read(stream: io.BufferedIOBase) -> 'NString': + """ + Create an NString object by reading a bstring from the provided stream. + + :param stream: Stream to read from. + :return: Resulting NString. + :raises: InvalidDataError + """ + return NString(read_bstring(stream)) + + def write(self, stream: io.BufferedIOBase) -> int: + """ + Write this NString to a stream. + + :param stream: Stream to write to. + :return: Number of bytes written. + """ + return write_bstring(stream, self.bytes) + + def __eq__(self, other) -> bool: + return isinstance(other, type(self)) and self.string == other.string + + def __repr__(self) -> str: + return '[N]' + self._string + + +def read_nstring(stream: io.BufferedIOBase) -> str: + """ + Read a name string from the provided stream. + See NString for constraints on name strings. + + :param stream: Stream to read from. + :return: Resulting string. + :raises: InvalidDataError + """ + return NString.read(stream).string + + +def write_nstring(stream: io.BufferedIOBase, string: str) -> int: + """ + Write a name string to a stream. + See NString for constraints on name strings. + + :param stream: Stream to write to. + :param string: String to write. + :return: Number of bytes written. + :raises: InvalidDataError + """ + return NString(string).write(stream) + + +class AString: + """ + Class for handling "ascii strings", which hold zero or more + ASCII characters (0x20 to 0x7e, inclusive). + + __init__ can be called with either a string or bytes object; + subsequent reading/writing should use the .string and + .bytes properties. + """ + _string = None # type: str + + def __init__(self, string_or_bytes: bytes or str): + """ + :param string_or_bytes: Content of the AString. + """ + if isinstance(string_or_bytes, str): + self.string = string_or_bytes + else: + self.bytes = string_or_bytes + + @property + def string(self) -> str: + return self._string + + @string.setter + def string(self, string: str): + if not all(0x20 <= ord(c) <= 0x7e for c in string): + raise InvalidDataError('Invalid a-string {}'.format(string)) + self._string = string + + @property + def bytes(self) -> bytes: + return self._string.encode('ascii') + + @bytes.setter + def bytes(self, bstring: bytes): + if not all(0x20 <= c <= 0x7e for c in bstring): + raise InvalidDataError('Invalid a-string {}'.format(bstring)) + self._string = bstring.decode('ascii') + + @staticmethod + def read(stream: io.BufferedIOBase) -> 'AString': + """ + Create an AString object by reading a bstring from the provided stream. + + :param stream: Stream to read from. + :return: Resulting AString. + :raises: InvalidDataError + """ + return AString(read_bstring(stream)) + + def write(self, stream: io.BufferedIOBase) -> int: + """ + Write this AString to a stream. + + :param stream: Stream to write to. + :return: Number of bytes written. + """ + return write_bstring(stream, self.bytes) + + def __eq__(self, other) -> bool: + return isinstance(other, type(self)) and self.string == other.string + + def __repr__(self) -> str: + return '[A]' + self._string + + +def read_astring(stream: io.BufferedIOBase) -> str: + """ + Read an ASCII string from the provided stream. + See AString for constraints on ASCII strings. + + :param stream: Stream to read from. + :return: Resulting string. + :raises: InvalidDataError + """ + return AString.read(stream).string + + +def write_astring(stream: io.BufferedIOBase, string: str) -> int: + """ + Write an ASCII string to a stream. + See AString for constraints on ASCII strings. + + :param stream: Stream to write to. + :param string: String to write. + :return: Number of bytes written. + :raises: InvalidDataError + """ + return AString(string).write(stream) + + +class ManhattanDelta: + """ + Class representing an axis-aligned ("Manhattan") vector. + + Has properties + .vertical (boolean, true if aligned along y-axis) + .value (int, signed length of the vector) + """ + vertical = None # type: bool + value = None # type: int + + def __init__(self, x: int, y: int): + """ + One of x or y _must_ be zero! + + :param x: x-displacement + :param y: y-displacement + """ + x = int(x) + y = int(y) + if x != 0: + if y != 0: + raise InvalidDataError('Non-Manhattan ManhattanDelta ({}, {})'.format(x, y)) + self.vertical = False + self.value = x + else: + self.vertical = True + self.value = y + + def as_list(self) -> List[int]: + """ + Return a list representation of this vector. + + :return: [x, y] + """ + xy = [0, 0] + xy[self.vertical] = self.value + return xy + + def as_uint(self) -> int: + """ + Return this vector encoded as an unsigned integer. + See ManhattanDelta.from_uint() for format details. + + :return: uint encoding of this vector. + """ + return (encode_sint(self.value) << 1) | self.vertical + + @staticmethod + def from_uint(n: int) -> 'ManhattanDelta': + """ + Construct a ManhattanDelta object from its unsigned integer encoding. + + The LSB of the encoded object is 1 if the vector is aligned to the + y-axis, or 0 if aligned to the x-axis. + The remaining bits are used to encode a signed integer containing + the signed length of the vector (see encode_sint() for format details). + + :param n: Unsigned integer representation of a ManhattanDelta vector. + :return: The ManhattanDelta object that was encoded by n. + """ + d = ManhattanDelta(0, 0) + d.value = decode_sint(n >> 1) + d.vertical = n & 0x01 + return d + + @staticmethod + def read(stream: io.BufferedIOBase) -> 'ManhattanDelta': + """ + Read a ManhattanDelta object from the provided stream. + + See .from_uint() for format details. + + :param stream: The stream to read from. + :return: The ManhattanDelta object that was read from the stream. + """ + n = read_uint(stream) + return ManhattanDelta.from_uint(n) + + def write(self, stream: io.BufferedIOBase) -> int: + """ + Write a ManhattanDelta object to the provided stream. + + See .from_uint() for format details. + + :param stream: The stream to write to. + :return: The number of bytes written. + """ + return write_uint(stream, self.as_uint()) + + def __eq__(self, other) -> bool: + return hasattr(other, as_list) and self.as_list() == other.as_list() + + def __repr__(self) -> str: + return '{}'.format(self.as_list()) + + +class OctangularDelta: + """ + Class representing an axis-aligned or 45-degree ("Octangular") vector. + + Has properties + .proj_mag (int, projection of the vector onto the x or y axis (non-zero)) + .octangle (int, bitfield: + bit 2: 1 if non-axis-aligned (non-Manhattan) + if Manhattan: + bit 1: 1 if direction is negative + bit 0: 1 if direction is y + if non-Manhattan: + bit 1: 1 if in lower half-plane + bit 0: 1 if x==-y + + Resulting directions: + 0: +x, 1: +y, 2: -x, 3: -y, + 4: +x+y, 5: -x+y, + 6: +x-y, 7: -x-y + ) + """ + proj_mag = None # type: int + octangle = None # type: int + + def __init__(self, x: int, y: int): + """ + Either abs(x)==abs(y), x==0, or y==0 _must_ be true! + + :param x: x-displacement + :param y: y-displacement + """ + x = int(x) + y = int(y) + if x == 0 or y == 0: + axis = (y != 0) + val = x | y + sign = val < 0 + self.proj_mag = abs(val) + self.octangle = (sign << 1) | axis + elif abs(x) == abs(y): + xn = (x < 0) + yn = (y < 0) + self.proj_mag = abs(x) + self.octangle = (1 << 2) | (yn << 1) | (xn != yn) + else: + raise InvalidDataError('Non-octangular delta! ({}, {})'.format(x, y)) + + def as_list(self) -> List[int]: + """ + Return a list representation of this vector. + + :return: [x, y] + """ + if self.octangle < 4: + xy = [0, 0] + axis = self.octangle & 0x01 > 0 + sign = self.octangle & 0x02 > 0 + xy[axis] = self.proj_mag * (1 - 2 * sign) + return xy + else: + yn = (self.octangle & 0x02) > 0 + xyn = (self.octangle & 0x01) > 0 + ys = 1 - 2 * yn + xs = ys * (1 - 2 * xyn) + v = self.proj_mag + return [v * xs, v * ys] + + def as_uint(self) -> int: + """ + Return this vector encoded as an unsigned integer. + See OctangularDelta.from_uint() for format details. + + :return: uint encoding of this vector. + """ + return (self.proj_mag << 3) | self.octangle + + @staticmethod + def from_uint(n: int) -> 'OctangularDelta': + """ + Construct an OctangularDelta object from its unsigned integer encoding. + + The low 3 bits are equal to .proj_mag, as specified in the class + docstring. + The remaining bits are used to encode an unsigned integer containing + the length of the vector. + + :param n: Unsigned integer representation of an OctangularDelta vector. + :return: The OctangularDelta object that was encoded by n. + """ + d = OctangularDelta(0, 0) + d.proj_mag = n >> 3 + d.octangle = n & 0b0111 + return d + + @staticmethod + def read(stream: io.BufferedIOBase) -> 'OctangularDelta': + """ + Read an OctangularDelta object from the provided stream. + + See .from_uint() for format details. + + :param stream: The stream to read from. + :return: The OctangularDelta object that was read from the stream. + """ + n = read_uint(stream) + return OctangularDelta.from_uint(n) + + def write(self, stream: io.BufferedIOBase) -> int: + """ + Write an OctangularDelta object to the provided stream. + + See .from_uint() for format details. + + :param stream: The stream to write to. + :return: The number of bytes written. + """ + return write_uint(stream, self.as_uint()) + + def __eq__(self, other) -> bool: + return hasattr(other, as_list) and self.as_list() == other.as_list() + + def __repr__(self) -> str: + return '{}'.format(self.as_list()) + + +class Delta: + """ + Class representing an arbitrary vector + + Has properties + .x (int) + .y (int) + """ + x = None # type: int + y = None # type: int + + def __init__(self, x: int, y: int): + """ + :param x: x-displacement + :param y: y-displacement + """ + x = int(x) + y = int(y) + self.x = x + self.y = y + + def as_list(self) -> List[int]: + """ + Return a list representation of this vector. + + :return: [x, y] + """ + return [self.x, self.y] + + @staticmethod + def read(stream: io.BufferedIOBase) -> 'Delta': + """ + Read a Delta object from the provided stream. + + The format consists of one or two unsigned integers. + The LSB of the first integer is 1 if a second integer is present. + If two integers are present, the remaining bits of the first + integer are an encoded signed integer (see encode_sint()), and + the second integer is an encoded signed_integer. + Otherwise, the remaining bits of the first integer are an encoded + OctangularData (see OctangularData.from_uint()). + + :param stream: The stream to read from. + :return: The Delta object that was read from the stream. + """ + n = read_uint(stream) + if (n & 0x01) == 0: + x, y = OctangularDelta.from_uint(n >> 1).as_list() + else: + x = decode_sint(n >> 1) + y = read_sint(stream) + return Delta(x, y) + + def write(self, stream: io.BufferedIOBase) -> int: + """ + Write a Delta object to the provided stream. + + See .from_uint() for format details. + + :param stream: The stream to write to. + :return: The number of bytes written. + """ + if self.x == 0 or self.y == 0 or abs(self.x) == abs(self.y): + return write_uint(stream, OctangularDelta(self.x, self.y).as_uint() << 1) + else: + size = write_uint(stream, (encode_sint(self.x) << 1) | 0x01) + size += write_uint(stream, encode_sint(self.y)) + return size + + def __eq__(self, other) -> bool: + return hasattr(other, as_list) and self.as_list() == other.as_list() + + def __repr__(self) -> str: + return '{}'.format(self.as_list()) + + +def read_repetition(stream: io.BufferedIOBase) -> repetition_t: + """ + Read a repetition entry from the given stream. + + :param stream: Stream to read from. + :return: The repetition entry. + """ + rtype = read_uint(stream) + if rtype == 0: + return ReuseRepetition.read(stream, rtype) + elif rtype in (1, 2, 3, 8, 9): + return GridRepetition.read(stream, rtype) + elif rtype in (4, 5, 6, 7, 10, 11): + return ArbitraryRepetition.read(stream, rtype) + + +def write_repetition(stream: io.BufferedIOBase, repetition: repetition_t) -> int: + """ + Write a repetition entry to the given stream. + + :param stream: Stream to write to. + :param repetition: The repetition entry to write. + :return: The number of bytes written. + """ + return repetition.write(stream) + + +class ReuseRepetition: + """ + Class representing a "reuse" repetition entry, which indicates that + the most recently written repetition should be reused. + """ + @staticmethod + def read(_stream: io.BufferedIOBase, _repetition_type: int) -> 'ReuseRepetition': + return ReuseRepetition() + + def write(self, stream: io.BufferedIOBase) -> int: + return write_uint(stream, 0) + + def __eq__(self, other) -> bool: + return isinstance(other, ReuseRepetition) + + def __repr__(self) -> str: + return 'ReuseRepetition' + + +class GridRepetition: + """ + Class representing a repetition entry denoting a 1D or 2D array + of regularly-spaced elements. The spacings are stored as one or + two lattice vectors, and the extent of the grid is stored as the + number of elements along each lattice vector. + + This class has properties + .a_vector ([xa: int, ya: int], vector specifying a center-to-center + displacement between adjacent elements in the grid.) + .b_vector ([xb: int, yb: int] or None, a second displacement, present if + a 2D grid is being specified.) + .a_count (int >= 1, number of elements along the grid axis specified by + .a_vector) + .b_count (int >= 1 or None, number of elements along the grid axis + specified by .b_vector) + """ + a_vector = None # type: List[int] + b_vector = None # type: List[int] or None + a_count = None # type: int + b_count = None # type: int or None + + def __init__(self, + a_vector: List[int], + a_count: int, + b_vector: List[int] = None, + b_count: int = None): + """ + :param a_vector: First lattice vector, of the form [x, y]. + Specifies center-to-center spacing between adjacent elements. + :param a_count: Number of elements in the a_vector direction. + :param b_vector: Second lattice vector, of the form [x, y]. + Specifies center-to-center spacing between adjacent elements. + Can be omitted when specifying a 1D array. + :param b_count: Number of elements in the b_vector direction. + Should be omitted if b_vector was omitted. + :raises: InvalidDataError if b_* inputs conflict with each other + or a_count < 1. + """ + self.a_vector = a_vector + self.b_vector = b_vector + self.a_count = a_count + self.b_count = b_count + + if self.b_vector is None or self.b_count is None: + if self.b_vector is not None or self.b_count is not None: + raise InvalidDataError('Repetition has only one of' + 'b_vector and b_count') + else: + if self.b_count < 1: + raise InvalidDataError('Repetition has too-small b_count') + if self.b_count < 2: + self.b_count = None + self.b_vector = None + print('Warning: removed b_count and b_vector since b_count == 1') + # TODO: warn here + + if self.a_count < 2: + raise InvalidDataError('Repetition has too-small x-count: ' + '{}'.format(a_count)) + self.a_vector = a_vector + self.b_vector = b_vector + self.a_count = a_count + self.b_count = b_count + + @staticmethod + def read(stream: io.BufferedIOBase, repetition_type: int) -> 'GridRepetition': + """ + Read a GridRepetition from a stream. + + :param stream: Stream to read from. + :param repetition_type: Repetition type as defined in OASIS repetition spec. + Valid types are 1, 2, 3, 8, 9. + :return: GridRepetition object read from stream. + :raises InvalidDataError if repetition_type is invalid. + """ + if repetition_type == 1: + na = read_uint(stream) + 2 + nb = read_uint(stream) + 2 + a_vector = [read_uint(stream), 0] + b_vector = [0, read_uint(stream)] + elif repetition_type == 2: + na = read_uint(stream) + 2 + nb = None + a_vector = [read_uint(stream), 0] + b_vector = None + elif repetition_type == 3: + na = read_uint(stream) + 2 + nb = None + a_vector = [0, read_uint(stream)] + b_vector = None + elif repetition_type == 8: + na = read_uint(stream) + 2 + nb = read_uint(stream) + 2 + a_vector = Delta.read(stream).as_list() + b_vector = Delta.read(stream).as_list() + elif repetition_type == 9: + na = read_uint(stream) + 2 + nb = None + a_vector = Delta.read(stream).as_list() + b_vector = None + else: + raise InvalidDataError('Invalid type for grid repetition ' + '{}'.format(repetition_type)) + return GridRepetition(a_vector, na, b_vector, nb) + + def write(self, stream: io.BufferedIOBase) -> int: + """ + Write the GridRepetition to a stream. + + A minimal representation is written (e.g., if b_count==1, + a 1D grid is written) + + :param stream: Stream to write to. + :return: Number of bytes written. + :raises: InvalidDataError if repetition is malformed. + """ + if self.b_vector is None or self.b_count is None: + if self.b_vector is not None or self.b_count is not None: + raise InvalidDataError('Malformed repetition {}'.format(self)) + + if self.a_vector[1] == 0: + size = write_uint(stream, 2) + size += write_uint(stream, self.a_count - 2) + size += write_uint(stream, self.a_vector[0]) + elif self.a_vector[0] == 0: + size = write_uint(stream, 3) + size += write_uint(stream, self.a_count - 2) + size += write_uint(stream, self.a_vector[1]) + else: + size = write_uint(stream, 9) + size += write_uint(stream, self.a_count - 2) + size += Delta(*self.a_vector).write(stream) + else: + if self.a_vector[1] == 0 and self.b_vector[0] == 0: + size = write_uint(stream, 1) + size += write_uint(stream, self.a_count - 2) + size += write_uint(stream, self.b_count - 2) + size += write_uint(stream, self.a_vector[0]) + size += write_uint(stream, self.b_vector[1]) + elif self.a_vector[0] == 0 and self.b_vector[1] == 0: + size = write_uint(stream, 1) + size += write_uint(stream, self.b_count - 2) + size += write_uint(stream, self.a_count - 2) + size += write_uint(stream, self.b_vector[0]) + size += write_uint(stream, self.a_vector[1]) + else: + size = write_uint(stream, 8) + size += write_uint(stream, self.a_count - 2) + size += write_uint(stream, self.b_count - 2) + size += Delta(*self.a_vector).write(stream) + size += Delta(*self.b_vector).write(stream) + return size + + def __eq__(self, other) -> bool: + return isinstance(other, type(self)) and \ + self.a_count == other.a_count and \ + self.b_count == other.b_count and \ + self.a_vector == other.a_vector and \ + self.b_vector == other.b_vector + + def __repr__(self) -> str: + return 'GridRepetition: ({} : {} | {} : {})'.format(self.a_count, self.a_vector, + self.b_count, self.b_vector) + + +class ArbitraryRepetition: + """ + Class representing a repetition entry denoting a 1D or 2D array + of arbitrarily-spaced elements. + + Properties: + .x_displacements (List[int], x-displacements between elements) + .y_displacements (List[int], y-displacements between elements) + """ + + x_displacements = None # type: List[int] + y_displacements = None # type: List[int] + + def __init__(self, + x_displacements: List[int], + y_displacements: List[int]): + """ + :param x_displacements: x-displacements between consecutive elements + :param y_displacements: y-displacements between consecutive elements + """ + self.x_displacements = x_displacements + self.y_displacements = y_displacements + + @staticmethod + def read(stream: io.BufferedIOBase, repetition_type) -> 'ArbitraryRepetition': + """ + Read an ArbitraryRepetition from a stream. + + :param stream: Stream to read from. + :param repetition_type: Repetition type as defined in OASIS repetition spec. + Valid types are 4, 5, 6, 7, 10, 11. + :return: ArbitraryRepetition object read from stream. + :raises InvalidDataError if repetition_type is invalid. + """ + if repetition_type == 4: + n = read_uint(stream) + 1 + x_displacements = [read_uint(stream) for _ in range(n)] + y_displacements = [0] * len(x_displacements) + elif repetition_type == 5: + n = read_uint(stream) + 1 + mult = read_uint(stream) + x_displacements = [mult * read_uint(stream) for _ in range(n)] + y_displacements = [0] * len(x_displacements) + if repetition_type == 6: + n = read_uint(stream) + 1 + y_displacements = [read_uint(stream) for _ in range(n)] + x_displacements = [0] * len(y_displacements) + elif repetition_type == 7: + n = read_uint(stream) + 1 + mult = read_uint(stream) + y_displacements = [mult * read_uint(stream) for _ in range(n)] + x_displacements = [0] * len(y_displacements) + elif repetition_type == 10: + n = read_uint(stream) + 1 + x_displacements = [] + y_displacements = [] + for _ in range(n): + x, y = Delta.read(stream).as_list() + x_displacements.append(x) + y_displacements.append(y) + elif repetition_type == 11: + n = read_uint(stream) + 1 + mult = read_uint(stream) + x_displacements = [] + y_displacements = [] + for _ in range(n): + x, y = Delta.read(stream).as_list() + x_displacements.append(x * mult) + y_displacements.append(y * mult) + else: + raise InvalidDataError('Invalid ArbitraryRepetition repetition_type') + return ArbitraryRepetition(x_displacements, y_displacements) + + def write(self, stream: io.BufferedIOBase) -> int: + """ + Write the ArbitraryRepetition to a stream. + + A minimal representation is attempted; common factors in the + displacements will be factored out, and lists of zeroes will + be omitted. + + :param stream: Stream to write to. + :return: Number of bytes written. + """ + def get_gcd(vals: List[int]) -> int: + """ + Get the greatest common denominator of a list of ints. + """ + if len(vals) == 1: + return vals[0] + + greatest = vals[0] + for v in vals[1:]: + greatest = math.gcd(greatest, v) + if greatest == 1: + break + return greatest + + x_gcd = get_gcd(self.x_displacements) + y_gcd = get_gcd(self.y_displacements) + if y_gcd == 0: + if x_gcd <= 1: + size = write_uint(stream, 4) + size += write_uint(stream, len(self.x_displacements) - 1) + size += sum(write_uint(stream, d) for d in self.x_displacements) + else: + size = write_uint(stream, 5) + size += write_uint(stream, len(self.x_displacements) - 1) + size += write_uint(stream, x_gcd) + size += sum(write_uint(stream, d // x_gcd) for d in self.x_displacements) + elif x_gcd == 0: + if y_gcd <= 1: + size = write_uint(stream, 6) + size += write_uint(stream, len(self.y_displacements) - 1) + size += sum(write_uint(stream, d) for d in self.y_displacements) + else: + size = write_uint(stream, 7) + size += write_uint(stream, len(self.y_displacements) - 1) + size += write_uint(stream, y_gcd) + size += sum(write_uint(stream, d // y_gcd) for d in self.y_displacements) + else: + gcd = math.gcd(x_gcd, y_gcd) + if gcd <= 1: + size = write_uint(stream, 10) + size += write_uint(stream, len(self.x_displacements) - 1) + size += sum(Delta(x, y).write(stream) + for x, y in zip(self.x_displacements, self.y_displacements)) + else: + size = write_uint(stream, 11) + size += write_uint(stream, len(self.x_displacements) - 1) + size += write_uint(stream, gcd) + size += sum(Delta(x // gcd, y // gcd).write(stream) + for x, y in zip(self.x_displacements, self.y_displacements)) + return size + + + def __eq__(self, other) -> bool: + return isinstance(other, type(self)) and self.x_displacements == other.x_displacements and self.y_displacements == other.y_displacements + + def __repr__(self) -> str: + return 'ArbitraryRepetition: x{} y{})'.format(self.x_displacements, self.y_displacements) + + +def read_point_list(stream: io.BufferedIOBase) -> List[List[int]]: + """ + Read a point list from a stream. + + :param stream: Stream to read from. + :return: Point list of the form [[x0, y0], [x1, y1], ...] + """ + list_type = read_uint(stream) + list_len = read_uint(stream) + #TODO: Implicit close point for 1del + if list_type == 0: + points = [] + for i in range(list_len): + point = [0, 0] + n = read_uint(stream) + if n == 0: + raise InvalidDataError('Zero-sized 1-delta') + point[i % 2] = n + points.append(point) + elif list_type == 1: + points = [] + for i in range(list_len): + point = [0, 0] + n = read_uint(stream) + if n == 0: + raise Exception('Zero-sized 1-delta') + point[(i + 1) % 2] = n + points.append(point) + elif list_type == 2: + points = [ManhattanDelta.read(stream).as_list() for _ in range(list_len)] + elif list_type == 3: + points = [OctangluarDelta.read(stream).as_list() for _ in range(list_len)] + elif list_type == 4: + points = [Delta.read(stream).as_list() for _ in range(list_len)] + elif list_type == 5: + deltas = [Delta.read(stream).as_list() for _ in range(list_len)] + if _USE_NUMPY: + delta_x, delta_y = zip(*deltas) + x = numpy.cumsum(delta_x) + y = numpy.cumsum(delta_y) + points = list(zip(x, y)) + else: + points = [] + x = 0 + y = 0 + for _ in range(list_len): + delta = Delta.read(stream) + x += delta.x + y += delta.y + points.append([x, y]) + else: + raise Exception('Invalid point list type') + return points + + +def write_point_list(stream: io.BufferedIOBase, + points: List[List[int]], + fast: bool = False, + implicit_closed: bool = True + ) -> int: + """ + Write a point list to a stream. + + :param stream: Stream to write to. + :param points: List of points, of the form [[x0, y0], [x1, y1], ...] + :param fast: If True, avoid searching for a compact representation for + the point list. + :param implicit_closed: Set to True if the list represents an implicitly + closed polygon, i.e. there is an implied line segment from points[-1] + to points[0]. If False, such segments are ignored, which can result in a + more compact representation for non-closed paths (e.g. a Manhattan + path with non-colinear endpoints). If unsure, use the default. + Default True. + :return: Number of bytes written. + """ + # If we're in a hurry, just write the points as arbitrary Deltas + if fast: + size = write_uint(stream, 4) + size += write_uint(stream, len(points)) + size += sum(Delta(x, y).write(stream) for x, y in points) + return size + + # If Manhattan with alternating direction, + # set one of h_first or v_first to True + # otherwise both end up False + previous = points[0] + h_first = previous[1] == 0 and len(points) % 2 == 0 + v_first = previous[0] == 0 and len(points) % 2 == 0 + for i, point in enumerate(points[1:]): + if (h_first and i % 2 == 0) or (v_first and i % 2 == 1): + if point[0] != previous[0] or point[1] == previous[1]: + h_first = False + v_first = False + break + else: + if point[1] != previous[1] or point[0] == previous[0]: + h_first = False + v_first = False + break + previous = point + + # If one of h_first or v_first, write a bunch of 1-deltas + if h_first: + size = write_uint(stream, 0) + size += write_uint(stream, len(points)) + size += sum(write_sint(stream, x + y) for x, y in points) + return size + elif v_first: + size = write_uint(stream, 1) + size += write_uint(stream, len(points)) + size += sum(write_sint(stream, x + y) for x, y in points) + return size + + # Try writing a bunch of Manhattan or Octangular deltas + list_type = None + try: + deltas = [ManhattanDelta(x, y) for x, y in points] + if implicit_closed: + ManhattanDelta(points[-1][0] - points[0][0], points[-1][1] - points[0][1]) + list_type = 2 + except: + try: + deltas = [OctangularDelta(x, y) for x, y in points] + if implicit_closed: + OctangularDelta(points[-1][0] - points[0][0], points[-1][1] - points[0][1]) + list_type = 3 + except: + pass + if list_type is not None: + size = write_uint(stream, list_type) + size += write_uint(stream, len(points)) + size += sum(d.write(stream) for d in deltas) + return size + + ''' + Looks like we need to write arbitrary deltas, + so we should check if it's better to write plain deltas, + or change-in-deltas. + ''' + # If it improves by decision_factor, use change-in-deltas + decision_factor = 4 + if _USE_NUMPY: + arr = numpy.array(points) + diff = numpy.diff(arr, axis=0) + if arr[1, :].sum() < diff.sum() * decision_factor: + list_type = 4 + deltas = [Delta(x, y) for x, y in points] + else: + list_type = 5 + deltas = [Delta(*points[0])] + [Delta(x, y) for x, y in diff] + else: + previous = [0, 0] + diff = [] + for point in points: + d = [point[0] - previous[0], point[1] - previous[1]] + previous = point + diff.append(d) + + if sum(sum(p) for p in points) < sum(sum(d) for d in diff) * decision_factor: + list_type = 4 + deltas = [Delta(x, y) for x, y in points] + else: + list_type = 5 + deltas = [Delta(x, y) for x, y in diff] + + size = write_uint(stream, list_type) + size += write_uint(stream, len(points)) + size += sum(d.write(stream) for d in deltas) + return size + + +class PropStringReference: + """ + Reference to a property string. + + Properties: + .ref (int, ID of the target) + .ref_type (Type, Type of the target: bytes, NString, or AString) + """ + ref = None # type: int + reference_type = None # type: Type + + def __init__(self, ref: int, ref_type: Type): + """ + :param ref: ID number of the target. + :param ref_type: Type of the target. One of bytes, NString, AString. + """ + self.ref = ref + self.ref_type = ref_type + + def __eq__(self, other) -> bool: + return isinstance(other, type(self)) and self.ref == other.ref and self.reference_type == other.reference_type + + def __repr__(self) -> str: + return '[{} : {}]'.format(self.ref_type, self.ref) + + +def read_property_value(stream: io.BufferedIOBase) -> property_value_t: + """ + Read a property value from a stream. + + The property value consists of a type (unsigned integer) and type- + dependent data. + + Data types: + 0...7: real number; property value type is reused for real number type + 8: unsigned integer + 9: signed integer + 10: ASCII string (AString) + 11: binary string (bytes) + 12: name string (NString) + 13: PropstringReference to AString + 14: PropstringReference to bstring (i.e., to bytes) + 15: PropstringReference to NString + + :param stream: Stream to read from. + :return: Value of the property, depending on type. + :raises: InvalidDataError if an invalid type is read. + """ + prop_type = read_uint(stream) + if 0 <= prop_type <= 7: + return read_real(stream, prop_type) + elif prop_type == 8: + return read_uint(stream) + elif prop_type == 9: + return read_sint(stream) + elif prop_type == 10: + return AString.read(stream) + elif prop_type == 11: + return read_bstring(stream) + elif prop_type == 12: + return NString.read(stream) + elif prop_type == 13: + ref_type = AString + ref = read_uint(stream) + return PropStringReference(ref, ref_type) + elif prop_type == 14: + ref_type = bytes + ref = read_uint(stream) + return PropStringReference(ref, ref_type) + elif prop_type == 15: + ref_type = NString + ref = read_uint(stream) + return PropStringReference(ref, ref_type) + else: + raise InvalidDataError('Invalid property type: {}'.format(prop_type)) + + +def write_property_value(stream: io.BufferedIOBase, + value: property_value_t, + force_real: bool = False, + force_signed_int: bool = False, + force_float32: bool = False + ) -> int: + """ + Write a property value to a stream. + + See read_property_value() for format details. + + :param stream: Stream to write to. + :param value: Property value to write. Can be an integer, a real number, + bytes (bstring), NString, AString, or a PropstringReference. + :param force_real: If True and value is an integer, writes an integer- + valued real number instead of a plain integer. Default False. + :param force_signed_int: If True and value is a positive integer, + writes a signed integer. Default false. + :param force_float32: If True and value is a float, writes a 32-bit + float (real number) instead of a 64-bit float. + :return: Number of bytes written. + """ + if isinstance(value, int) and not force_real: + if force_signed_int or value < 0: + size = write_uint(stream, 9) + size += write_sint(stream, value) + else: + size = write_uint(stream, 8) + size += write_uint(stream, value) + elif isinstance(value, real_t): + size = write_real(stream, value, force_float32) + elif isinstance(value, AString): + size = write_uint(stream, 10) + size += value.write(stream) + elif isinstance(value, bytes): + size = write_uint(stream, 11) + size += write_bstring(stream, value) + elif isinstance(value, NString): + size = write_uint(stream, 12) + size += value.write(stream) + elif isinstance(value, PropStringReference): + if value.ref_type == AString: + size = write_uint(stream, 13) + elif value.ref_type == bytes: + size = write_uint(stream, 14) + if value.ref_type == AString: + size = write_uint(stream, 15) + size += write_uint(stream, value.ref) + else: + raise Exception('Invalid property type: {} ({})'.format(type(value), value)) + return size + + +def read_interval(stream: io.BufferedIOBase) -> Tuple[int or None]: + """ + Read an interval from a stream. + These are used for storing layer info. + + The format consists of a type specifier (unsigned integer) and + a variable number of integers: + type 0: 0, inf (no data) + type 1: 0, b (unsigned integer b) + type 2: a, inf (unsigned integer a) + type 3: a, a (unsigned integer a) + type 4: a, b (unsigned integers a, b) + + :param stream: Stream to read from. + :return: (lower, upper), where + lower can be None if there is an implicit lower bound of 0 + upper can be None if there is no upper bound (inf) + """ + interval_type = read_uint(stream) + if interval_type == 0: + return None, None + elif interval_type == 1: + return None, read_uint(stream) + elif interval_type == 2: + return read_uint(stream), None + elif interval_type == 3: + v = read_uint(stream) + return v, v + elif interval_type == 4: + return read_uint(stream), read_uint(stream) + + +def write_interval(stream: io.BufferedIOBase, + min_bound: int or None = None, + max_bound: int or None = None + ) -> int: + """ + Write an interval to a stream. + Used for layer data; see read_interval for format details. + + :param stream: Stream to write to. + :param min_bound: Lower bound on the interval, can be None (implicit 0, default) + :param max_bound: Upper bound on the interval, can be None (unbounded, default) + :return: Number of bytes written. + """ + if min_bound is None: + if max_bound is None: + return write_uint(stream, 0) + else: + return write_uint(stream, 1) + write_uint(stream, max_bound) + else: + if max_bound is None: + return write_uint(stream, 2) + write_uint(stream, min_bound) + else: + size = write_uint(stream, 3) + size += write_uint(stream, min_bound) + size += write_uint(stream, max_bound) + return size + + +class OffsetEntry: + """ + Entry for the file's offset table. + + Properties: + .strict (bool, If False, the records pointed to by this + offset entry may also appear elsewhere in the file. + If True, all records of the type pointed to by this + offset entry must be present in a contiuous block at + the specified offset [pad records also allowed]. + Additionally: + All references to strict-mode records must be + explicit (using reference_number). + The offset may point to an encapsulating CBlock + record, if the first record in that CBlock is + of the target record type. A strict mode table + cannot begin in the middle of a CBlock. + ) + .offset (int, offset from the start of the file; may be 0 + for records that are not present.) + """ + strict = False # type: bool + offset = 0 # type: int + + def __init__(self, strict: bool = False, offset: int = 0): + """ + :param strict: True if the records referenced are written in + strict mode (see class docstring). Default False. + :param offset: Offset from the start of the file for the + referenced records; may be 0 if records are absent. + Default 0. + """ + self.strict = strict + self.offset = offset + + @staticmethod + def read(stream: io.BufferedIOBase) -> 'OffsetEntry': + """ + Read an offset entry from a stream. + + :param stream: Stream to read from. + :return: Offset entry that was read. + """ + entry = OffsetEntry() + entry.strict = read_uint(stream) > 0 + entry.offset = read_uint(stream) + return entry + + def write(self, stream: io.BufferedIOBase) -> int: + """ + Write this offset entry to a stream. + + :param stream: Stream to write to. + :return: Number of bytes written + """ + return write_uint(stream, self.strict) + write_uint(stream, self.offset) + + def __repr__(self) -> str: + return 'Offset(s: {}, o: {})'.format(self.strict, self.offset) + + +class OffsetTable: + """ + Offset table, containing OffsetEntry data for each of 6 different + record types, + + CellName + TextString + PropName + PropString + LayerName + XName + + which are stored in the above order in the file's offset table. + + Proerties: + .cellnames (OffsetEntry) + .textstrings (OffsetEntry) + .propnames (OffsetEntry) + .propstrings (OffsetEntry) + .layernames (OffsetEntry) + .xnames (OffsetEntry) + """ + cellnames = None # type: OffsetEntry + textstrings= None # type: OffsetEntry + propnames = None # type: OffsetEntry + propstrings = None # type: OffsetEntry + layernames = None # type: OffsetEntry + xnames = None # type: OffsetEntry + + def __init__(self, + cellnames: OffsetEntry = None, + textstrings: OffsetEntry = None, + propnames: OffsetEntry = None, + propstrings: OffsetEntry = None, + layernames: OffsetEntry = None, + xnames: OffsetEntry = None): + """ + All parameters default to a non-strict entry with offset 0. + + :param cellnames: OffsetEntry for CellName records. + :param textstrings: OffsetEntry for TextString records. + :param propnames: OffsetEntry for PropName records. + :param propstrings: OffsetEntry for PropString records. + :param layernames: OffsetEntry for LayerNamerecords. + :param xnames: OffsetEntry for XName records. + """ + if cellnames is None: + cellnames = OffsetEntry() + if textstrings is None: + textstrings = OffsetEntry() + if propnames is None: + propnames = OffsetEntry() + if propstrings is None: + propstrings = OffsetEntry() + if layernames is None: + layernames = OffsetEntry() + if xnames is None: + xnames = OffsetEntry() + + self.cellnames = cellnames + self.textstrings = textstrings + self.propnames = propnames + self.propstrings = propstrings + self.layernames = layernames + self.xnames = xnames + + @staticmethod + def read(stream: io.BufferedIOBase) -> 'OffsetTable': + """ + Read an offset table from a stream. + See class docstring for format details. + + :param stream: Stream to read from. + :return: The offset table that was read. + """ + table = OffsetTable() + table.cellnames = OffsetEntry.read(stream) + table.textstrings = OffsetEntry.read(stream) + table.propnames = OffsetEntry.read(stream) + table.propstrings = OffsetEntry.read(stream) + table.layernames = OffsetEntry.read(stream) + table.xnames = OffsetEntry.read(stream) + return table + + def write(self, stream: io.BufferedIOBase) -> int: + """ + Write this offset table to a stream. + See class docstring for format details. + + :param stream: Stream to write to. + :return: Number of bytes written. + """ + size = self.cellnames.write(stream) + size += self.textstrings.write(stream) + size += self.propnames.write(stream) + size += self.propstrings.write(stream) + size += self.layernames.write(stream) + size += self.xnames.write(stream) + return size + + def __repr__(self) -> str: + return 'OffsetTable({})'.format([self.cellnames, self.textstrings, self.propnames, + self.propstrings, self.layernames, self.xnames]) + + +def read_u32(stream: io.BufferedIOBase) -> int: + """ + Read a 32-bit unsigned integer (little endian) from a stream. + + :param stream: Stream to read from. + :return: The integer that was read. + """ + b = _read(stream, 4) + return struct.unpack(' int: + """ + Write a 32-bit unsigned integer (little endian) to a stream. + + :param stream: Stream to write to. + :param n: Integer to write. + :return: The number of bytes written (4). + :raises: SignedError if n is negative. + """ + if n < 0: + raise SignedError('Negative u32: {}'.format(n)) + return stream.write(struct.pack(' 2: + raise InvalidDataError('Invalid validation type') + if checksum_type == 0 and checksum is not None: + raise InvalidDataError('Validation type 0 shouldn\'t have a checksum') + self.checksum_type = checksum_type + self.checksum = checksum + + @staticmethod + def read(stream: io.BufferedIOBase) -> 'Validation': + """ + Read a validation entry from a stream. + See class docstring for format details. + + :param stream: Stream to read from. + :return: The validation entry that was read. + :raises: InvalidDataError if an invalid validation type was encountered. + """ + checksum_type = read_uint(stream) + if checksum_type == 0: + checksum = None + elif checksum_type == 1: + checksum = read_u32(stream) + elif checksum_type == 2: + checksum = read_u32(stream) + else: + raise InvalidDataError('Invalid validation type!') + return Validation(checksum_type, checksum) + + def write(self, stream: io.BufferedIOBase) -> int: + """ + Write this validation entry to a stream. + See class docstring for format details. + + :param stream: Stream to write to. + :return: Number of bytes written. + """ + if self.checksum_type == 0: + return write_uint(stream, 0) + elif self.checksum_type == 1: + return write_uint(stream, 1) + write_u32(stream, self.checksum) + elif self.checksum_type == 2: + return write_uint(stream, 2) + write_u32(stream, self.checksum) + + def __repr__(self) -> str: + return 'Validation(type: {} sum: {})'.format(self.checksum_type, self.checksum) + + +def write_magic_bytes(stream: io.BufferedIOBase) -> int: + """ + Write the magic byte sequence to a stream. + + :param stream: Stream to write to. + :return: Number of bytes written. + """ + return stream.write(MAGIC_BYTES) + + +def read_magic_bytes(stream: io.BufferedIOBase): + """ + Read the magic byte sequence from a stream. + Raise an InvalidDataError if it was not found. + + :param stream: Stream to read from. + :raises: InvalidDataError if the sequence was not found. + """ + magic = _read(stream, len(MAGIC_BYTES)) + if magic != MAGIC_BYTES: + raise InvalidDataError('Could not read magic bytes, ' + 'found {} : {}'.format(magic, magic.decode())) + diff --git a/fatamorgana/main.py b/fatamorgana/main.py new file mode 100644 index 0000000..f8dec6a --- /dev/null +++ b/fatamorgana/main.py @@ -0,0 +1,437 @@ +""" +This module contains data structures and functions for reading from and + writing to whole OASIS layout files, and provides a few additional + abstractions for the data contained inside them. +""" +import io + +from . import records +from .records import Modals +from .basic import OffsetEntry, OffsetTable, NString, AString, real_t, Validation, \ + read_magic_bytes, write_magic_bytes, read_uint, EOFError, \ + InvalidDataError, InvalidRecordError + + +__author__ = 'Jan Petykiewicz' + + +class FileModals: + """ + File-scoped modal variables + """ + cellname_implicit = None # type: bool or None + propname_implicit = None # type: bool or None + xname_implicit = None # type: bool or None + textstring_implicit = None # type: bool or None + propstring_implicit = None # type: bool or None + cellname_implicit = None # type: bool or None + + within_cell = False # type: bool + within_cblock = False # type: bool + end_has_offset_table = None # type: bool + started = False # type: bool + + +class OasisLayout: + """ + Representation of a full OASIS layout file. + + Names and strings are stored in dicts, indexed by reference number. + Layer names and properties are stored directly using their associated + record objects. + Cells are stored using Cell objects (different from Cell record objects). + + Properties: + File properties: + .version AString: Version string ('1.0') + .unit real number: grid steps per micron + .validation Validation: checksum data + + Names: + .cellnames Dict[int, NString] + .propnames Dict[int, NString] + .xnames Dict[int, XName] + + Strings: + .textstrings Dict[int, AString] + .propstrings Dict[int, AString] + + Data: + .layers List[records.LayerName] + .properties List[records.Property] + .cells List[Cell] + """ + version = None # type: AString + unit = None # type: real_t + validation = None # type: Validation + + properties = None # type: List[records.Property] + cells = None # type: List[Cell] + + cellnames = None # type: Dict[int, NString] + propnames = None # type: Dict[int, NString] + xnames = None # type: Dict[int, XName] + + textstrings = None # type: Dict[int, AString] + propstrings = None # type: Dict[int, AString] + layers = None # type: List[records.LayerName] + + + def __init__(self, unit: real_t, validation: Validation = None): + """ + :param unit: Real number (i.e. int, float, or Fraction), grid steps per micron. + :param validation: Validation object containing checksum data. + Default creates a Validation object of the "no checksum" type. + """ + if validation is None: + validation = Validation(0) + + self.unit = unit + self.validation = validation + self.version = AString("1.0") + self.properties = [] + self.cells = [] + self.cellnames = {} + self.propnames = {} + self.xnames = {} + self.textstrings = {} + self.propstrings = {} + self.layers = [] + + @staticmethod + def read(stream: io.BufferedIOBase) -> 'OasisLayout': + """ + Read an entire .oas file into an OasisLayout object. + + :param stream: Stream to read from. + :return: New OasisLayout object. + """ + file_state = FileModals() + modals = Modals() + layout = OasisLayout(unit=None) + + read_magic_bytes(stream) + + while not layout.read_record(stream, modals, file_state): + pass + return layout + + def read_record(self, + stream: io.BufferedIOBase, + modals: Modals, + file_state: FileModals + ) -> bool: + """ + Read a single record of unspecified type from a stream, adding its + contents into this OasisLayout object. + + :param stream: Stream to read from. + :param modals: Modal variable data, used to fill unfilled record + fields and updated using filled record fields. + :param file_state: File status data. + :return: True if EOF was reached without error, False otherwise. + :raises: InvalidRecordError from unexpected records; + InvalidDataError from within record parsers. + """ + try: + record_id = read_uint(stream) + except EOFError as e: + if file_state.within_cblock: + return True + else: + raise e + + # TODO logging + print(record_id, stream.tell()) + + # CBlock + if record_id == 34: + if file_state.within_cblock: + raise InvalidRecordError('Nested CBlock') + record = records.CBlock.read(stream, record_id) + decoded_data = record.decompress() + + file_state.within_cblock = True + decoded_stream = io.BytesIO(decoded_data) + while not self.read_record(decoded_stream, modals, file_state): + pass + file_state.within_cblock = False + return False + + # Make sure order is valid (eg, no out-of-cell geometry) + if not file_state.started and record_id != 1: + raise InvalidRecordError('Non-Start record {} before Start'.format(record_id)) + if record_id == 1: + if file_state.started: + raise InvalidRecordError('Duplicate Start record') + else: + file_state.started = True + if record_id == 2 and file_state.within_cblock: + raise InvalidRecordError('End within CBlock') + + if record_id in (0, 1, 2, 28, 29): + pass + elif record_id in range(3, 13) or record_id in (28, 29): + file_state.within_cell = False + elif record_id in range(15, 29) or record_id in (32, 33): + if not file_state.within_cell: + raise Exception('Geometry outside Cell') + elif record_id == 13: + file_state.within_cell = True + else: + raise InvalidRecordError('Unknown record id: {}'.format(record_id)) + + if record_id == 0: + # Pad + pass + elif record_id == 1: + record = records.Start.read(stream, record_id) + record.merge_with_modals(modals) + self.unit = record.unit + self.version = record.version + file_state.end_has_offset_table = record.offset_table is None + # TODO Offset table strict check + elif record_id == 2: + record = records.End.read(stream, record_id, file_state.end_has_offset_table) + record.merge_with_modals(modals) + self.validation = record.validation + if not len(stream.read(1)) == 0: + raise InvalidRecordError('Stream continues past End record') + return True + elif record_id in (3, 4): + implicit = record_id == 3 + if file_state.cellname_implicit is None: + file_state.cellname_implicit = implicit + elif file_state.cellname_implicit != implicit: + raise InvalidRecordError('Mix of implicit and non-implicit cellnames') + + record = records.CellName.read(stream, record_id) + record.merge_with_modals(modals) + key = record.reference_number + if key is None: + key = len(self.cellnames) + self.cellnames[key] = record.nstring + elif record_id in (5, 6): + implicit = record_id == 5 + if file_state.textstring_implicit is None: + file_state.textstring_implicit = implicit + elif file_state.textstring_implicit != implicit: + raise InvalidRecordError('Mix of implicit and non-implicit textstrings') + + record = records.TextString.read(stream, record_id) + record.merge_with_modals(modals) + key = record.reference_number + if key is None: + key = len(self.textstrings) + self.textstrings[key] = record.astring + elif record_id in (7, 8): + implicit = record_id == 7 + if file_state.propname_implicit is None: + file_state.propname_implicit = implicit + elif file_state.propname_implicit != implicit: + raise InvalidRecordError('Mix of implicit and non-implicit propnames') + + record = records.PropName.read(stream, record_id) + record.merge_with_modals(modals) + key = record.reference_number + if key is None: + key = len(self.propnames) + self.propnames[key] = record.nstring + elif record_id in (9, 10): + implicit = record_id == 9 + if file_state.propstring_implicit is None: + file_state.propstring_implicit = implicit + elif file_state.propstring_implicit != implicit: + raise InvalidRecordError('Mix of implicit and non-implicit propstrings') + + record = records.PropString.read(stream, record_id) + record.merge_with_modals(modals) + key = record.reference_number + if key is None: + key = len(self.propstrings) + self.propstrings[key] = record.astring + elif record_id in (11, 12): + record = records.LayerName.read(stream, record_id) + record.merge_with_modals(modals) + self.layers.append(record) + elif record_id in (28, 29): + record = records.Property.read(stream, record_id) + record.merge_with_modals(modals) + if not file_state.within_cell: + self.properties.append(record) + else: + self.cells[-1].properties.append(record) + elif record_id in (30, 31): + implicit = record_id == 30 + if file_state.xname_implicit is None: + file_state.xname_implicit = implicit + elif file_state.xname_implicit != implicit: + raise InvalidRecordError('Mix of implicit and non-implicit xnames') + + record = records.XName.read(stream, record_id) + record.merge_with_modals(modals) + key = record.reference_number + if key is None: + key = len(self.xnames) + self.xnames[key] = XName.from_record(record) + + # + # Cell and elements + # + elif record_id in (13, 14): + record = records.Cell.read(stream, record_id) + record.merge_with_modals(modals) + self.cells.append(Cell(record.name)) + elif record_id in (15, 16): + record = records.XYMode.read(stream, record_id) + record.merge_with_modals(modals) + elif record_id in (17, 18): + record = records.Placement.read(stream, record_id) + record.merge_with_modals(modals) + self.cells[-1].placements.append(record) + elif record_id in _GEOMETRY: + record = _GEOMETRY[record_id].read(stream, record_id) + record.merge_with_modals(modals) + self.cells[-1].geometry.append(record) + else: + raise InvalidRecordError('Unknown record id: {}'.format(record_id)) + return False + + def write(self, stream: io.BufferedIOBase) -> int: + """ + Write this object in OASIS fromat to a stream. + + :param stream: Stream to write to. + :return: Number of bytes written. + :raises: InvalidDataError if contained records are invalid. + """ + modals = Modals() + + size = 0 + size += write_magic_bytes(stream) + size += records.Start(self.unit, self.version).dedup_write(stream, modals) + + cellnames_offset = OffsetEntry(False, size) + size += sum(records.CellName(name, refnum).dedup_write(stream, modals) + for refnum, name in self.cellnames.items()) + + propnames_offset = OffsetEntry(False, size) + size += sum(records.PropName(name, refnum).dedup_write(stream, modals) + for refnum, name in self.propnames.items()) + + xnames_offset = OffsetEntry(False, size) + size += sum(records.XName(x.attribute, x.string, refnum).dedup_write(stream, modals) + for refnum, x in self.xnames.items()) + + textstrings_offset = OffsetEntry(False, size) + size += sum(records.TextString(s, refnum).dedup_write(stream, modals) + for refnum, s in self.textstrings.items()) + + propstrings_offset = OffsetEntry(False, size) + size += sum(records.PropString(s, refnum).dedup_write(stream, modals) + for refnum, s in self.propstrings.items()) + + layernames_offset = OffsetEntry(False, size) + size += sum(r.dedup_write(stream, modals) for r in self.layers) + + size += sum(p.dedup_write(stream, modals) for p in self.properties) + + size += sum(c.dedup_write(stream, modals) for c in self.cells) + + offset_table = OffsetTable( + cellnames_offset, + textstrings_offset, + propnames_offset, + propstrings_offset, + layernames_offset, + xnames_offset, + ) + size += records.End(self.validation, offset_table).dedup_write(stream, modals) + return size + + +class Cell: + """ + Representation of an OASIS cell. + + Properties: + .name NString or int (CellName reference number) + + .properties List of records.Property + .placements List of records.Placement + .geometry List of geometry record objectes + """ + name = None # type: NString or int + properties = None # type: List[records.Property] + placements = None # type: List[records.Placement] + geometry = None # type: List[records.geometry_t] + + def __init__(self, name: NString or int): + """ + :param name: NString or int (CellName reference number) + """ + self.name = name + self.properties = [] + self.placements = [] + self.geometry = [] + + def dedup_write(self, stream: io.BufferedIOBase, modals: Modals) -> int: + """ + Write this cell to a stream, using the provided modal variables to + deduplicate any repeated data. + + :param stream: Stream to write to. + :param modals: Modal variables to use for deduplication. + :return: Number of bytes written. + :raises: InvalidDataError if contained records are invalid. + """ + size = records.Cell(self.name).dedup_write(stream, modals) + size += sum(p.dedup_write(stream, modals) for p in self.properties) + size += sum(p.dedup_write(stream, modals) for p in self.placements) + size += sum(g.dedup_write(stream, modals) for g in self.geometry) + return size + + +class XName: + """ + Representation of an XName. + + This class is effectively a simplified form of a records.XName, + with the reference data stripped out. + """ + attribute = None # type: int + bstring = None # type: bytes + + def __init__(self, attribute: int, bstring: bytes): + """ + :param attribute: Attribute number. + :param bstring: Binary data. + """ + self.attribute = attribute + self.bstring = bstring + + @staticmethod + def from_record(record: records.XName) -> 'XName': + """ + Create an XName object from a records.XName record. + + :param record: XName record to use. + :return: XName object. + """ + return XName(record.attribute, record.bstring) + + +# Mapping from record id to record class. +_GEOMETRY = { + 19: records.Text, + 20: records.Rectangle, + 21: records.Polygon, + 22: records.Path, + 23: records.Trapezoid, + 24: records.Trapezoid, + 25: records.Trapezoid, + 26: records.CTrapezoid, + 27: records.Circle, + 32: records.XElement, + 33: records.XGeometry, + } diff --git a/fatamorgana/records.py b/fatamorgana/records.py new file mode 100644 index 0000000..d99fcbb --- /dev/null +++ b/fatamorgana/records.py @@ -0,0 +1,2434 @@ +""" +This module contains all 'record' or 'block'-level datastructures and their + associated writing and parsing code, as well as a few helper functions. + +Additionally, this module contains definitions for the record-level modal + variables (stored in the Modals class). + +Higher-level code (e.g. monitoring for combinations of records with + implicit and explicit references, code for deciding which record type to + parse, or code for dealing with nested records in a CBlock) should live + in main.py instead. +""" +from abc import ABCMeta, abstractmethod +from typing import List, Dict, Tuple +import copy +import math +import zlib +import io + +from .basic import AString, NString, repetition_t, property_value_t, real_t, \ + ReuseRepetition, OffsetTable, Validation, read_point_list, read_property_value, \ + read_bstring, read_uint, read_sint, read_real, read_repetition, read_interval, \ + write_bstring, write_uint, write_sint, write_real, write_interval, write_point_list, \ + write_property_value, read_bool_byte, write_bool_byte, read_byte, write_byte, \ + InvalidDataError, PathExtensionScheme + +''' + Type definitions +''' +geometry_t = 'Text' or 'Rectangle' or 'Polygon' or 'Path' or 'Trapezoid' or \ + 'CTrapezoid' or 'Circle' or 'XElement' or 'XGeometry' +pathextension_t = Tuple['PathExtensionScheme' or int] + + +class Modals: + """ + Modal variables, used to store data about previously-written ori + -read records. + """ + repetition = None # type: repetition_t or None + placement_x = 0 # type: int + placement_y = 0 # type: int + placement_cell = None # type: int or NString or None + layer = None # type: int or None + datatype = None # type: int or None + text_layer = None # type: int or None + text_datatype = None # type: int or None + text_x = 0 # type: int + text_y = 0 # type: int + text_string = None # type: AString or int or None + geometry_x = 0 # type: int + geometry_y = 0 # type: int + xy_relative = False # type: bool + geometry_w = None # type: int or None + geometry_h = None # type: int or None + polygon_point_list = None # type: List[List[int]] or None + path_halfwidth = None # type: int or None + path_point_list = None # type: List[List[int]] or None + path_extension_start = None # type: pathextension_t or None + path_extension_end = None # type: pathextension_t or None + ctrapezoid_type = None # type: int or None + circle_radius = None # type: int or None + property_value_list = None # type: List[property_value_t] or None + property_name = None # type: int or NString or None + property_is_standard = None # type: bool or None + + def __init__(self): + self.reset() + + def reset(self): + """ + Resets all modal variables to their default values. + Default values are: + 0 for placement_{x,y}, text_{x,y}, geometry_{x,y} + False for xy_relative + Undefined (None) for all others + """ + self.repetition = None + self.placement_x = 0 + self.placement_y = 0 + self.placement_cell = None + self.layer = None + self.datatype = None + self.text_layer = None + self.text_datatype = None + self.text_x = 0 + self.text_y = 0 + self.text_string = None + self.geometry_x = 0 + self.geometry_y = 0 + self.xy_relative = False + self.geometry_w = None + self.geometry_h = None + self.polygon_point_list = None + self.path_halfwidth = None + self.path_point_list = None + self.path_extension_start = None + self.path_extension_end = None + self.ctrapezoid_type = None + self.circle_radius = None + self.property_value_list = None + self.property_name = None + self.property_is_standard = None + + +''' + + Records + +''' + +class Record(metaclass=ABCMeta): + """ + Common interface for records. + """ + @abstractmethod + def merge_with_modals(self, modals: Modals): + """ + Copy all defined values from this record into the modal variables. + Fill all undefined values in this record from the modal variables. + + :param modals: Modal variables to merge with. + """ + pass + + @abstractmethod + def deduplicate_with_modals(self, modals: Modals): + """ + Check all defined values in this record against those in the + modal variables. If any values are equal, remove them from + the record and indicate that the modal variables should be + used instead. Update the modal variables using the remaining + (unequal) values. + + :param modals: Modal variables to deduplicate with. + """ + pass + + @staticmethod + @abstractmethod + def read(stream: io.BufferedIOBase, record_id: int) -> 'Record': + """ + Read a record of this type from a stream. + This function does not merge with modal variables. + + :param stream: Stream to read from. + :param record_id: Record id of the record to read. The + record id is often used to specify which variant + of the record is stored. + :return: The record that was read. + :raises: InvalidDataError if the record is malformed. + """ + pass + + @abstractmethod + def write(self, stream: io.BufferedIOBase) -> int: + """ + Write this record to a stream as-is. + This function does not merge or deduplicate with modal variables. + + :param stream: Stream to write to. + :return: Number of bytes written. + :raises: InvalidDataError if the record contains invalid data. + """ + pass + + def dedup_write(self, stream: io.BufferedIOBase, modals: Modals) -> int: + """ + Run .deduplicate_with_modals() and then .write() to the stream. + + :param stream: Stream to write to. + :param modals: Modal variables to merge with. + :return: Number of bytes written + :raises: InvalidDataError if the record contains invalid data. + """ + # TODO logging + #print(type(self), stream.tell()) + self.deduplicate_with_modals(modals) + return self.write(stream) + + def copy(self) -> 'Record': + """ + Perform a deep copy of this record. + + :return: A deep copy of this record. + """ + return copy.deepcopy(self) + + +def read_refname(stream: io.BufferedIOBase, + is_present: bool, + is_reference: bool + ) -> None or int or NString: + """ + Helper function for reading a possibly-absent, possibly-referenced NString. + + :param stream: Stream to read from. + :param is_present: If False, read nothing and return None + :param is_reference: If True, read a uint (reference id), + otherwise read an NString. + :return: None, reference id, or NString + """ + if not is_present: + return None + elif is_reference: + return read_uint(stream) + else: + return NString.read(stream) + + +def read_refstring(stream: io.BufferedIOBase, + is_present: bool, + is_reference: bool + ) -> None or int or AString: + """ + Helper function for reading a possibly-absent, possibly-referenced AString. + + :param stream: Stream to read from. + :param is_present: If False, read nothing and return None + :param is_reference: If True, read a uint (reference id), + otherwise read an AString. + :return: None, reference id, or AString + """ + if not is_present: + return None + elif is_reference: + return read_uint(stream) + else: + return AString.read(stream) + + +class Pad(Record): + """ + Pad record (ID 0) + """ + def merge_with_modals(self, modals: Modals): + pass + + def deduplicate_with_modals(self, modals: Modals): + pass + + @staticmethod + def read(stream: io.BufferedIOBase, record_id: int) -> 'Pad': + if record_id != 0: + raise InvalidDataError('Invalid record id for Pad ' + '{}'.format(record_id)) + record = Pad() + return record + + def write(self, stream: io.BufferedIOBase) -> int: + return write_uint(stream, 0) + + +class XYMode(Record): + """ + XYMode record (ID 15, 16) + + Properties: + .relative (bool, default False) + """ + relative = False # type: bool + + @property + def absolute(self) -> bool: + return not relative + + @absolute.setter + def absolute(self, b: bool): + self.relative = not b + + def __init__(self, relative: bool): + """ + :param relative: True if the mode is 'relative', False if 'absolute'. + """ + self.relative = value + + def merge_with_modals(self, modals: Modals): + modals.xy_relative = self.relative + + def deduplicate_with_modals(self, modals: Modals): + pass + + @staticmethod + def read(stream: io.BufferedIOBase, record_id: int) -> 'XYMode': + if record_id not in (15, 16): + raise InvalidDataError('Invalid record id for XYMode') + return XYMode(record_id == 16) + + def write(self, stream: io.BufferedIOBase) -> int: + return write_uint(stream, 15 + self.relative) + + +class Start(Record): + """ + Start Record (ID 1) + + Properties: + .version (AString, "1.0") + .unit (positive real number, grid steps per micron) + .offset_table (OffsetTable or None, if None then table must be + placed in the End record) + """ + version = None # type: AString + unit = None # type: real_t + offset_table = None # type: OffsetTable + + def __init__(self, + unit: real_t, + version: AString or str = None, + offset_table: OffsetTable = None): + """ + :param unit: Grid steps per micron (positive real number) + :param version: Version string, default "1.0" + :param offset_table: OffsetTable for the file, or None to place + it in the End record instead. + """ + if unit <= 0: + raise InvalidDataError('Non-positive unit: {}'.format(unit)) + if math.isnan(unit): + raise InvalidDataError('NaN unit') + if math.isinf(unit): + raise InvalidDataError('Non-finite unit') + self.unit = unit + + if version is None: + version = AString('1.0') + if isinstance(version, AString): + self.version = version + else: + self.version = AString(version) + + if self.version.string != '1.0': + raise InvalidDataError('Invalid version string, ' + 'only "1.0" is allowed: ' + + str(self.version.string)) + self.offset_table = offset_table + + def merge_with_modals(self, modals: Modals): + modals.reset() + + def deduplicate_with_modals(self, modals: Modals): + modals.reset() + + @staticmethod + def read(stream: io.BufferedIOBase, record_id: int) -> 'Start': + if record_id != 1: + raise InvalidDataError('Invalid record id for Start: ' + '{}'.format(record_id)) + version = AString.read(stream) + unit = read_real(stream) + has_offset_table = read_uint(stream) == 0 + if has_offset_table: + offset_table = OffsetTable.read(stream) + else: + offset_table = None + return Start(unit, version, offset_table) + + def write(self, stream: io.BufferedIOBase) -> int: + size = write_uint(stream, 1) + size += self.version.write(stream) + size += write_real(stream, self.unit) + size += write_uint(stream, self.offset_table is None) + if self.offset_table is not None: + size += self.offset_table.write(stream) + return size + + +class End(Record): + """ + End record (ID 2) + + The end record is always padded to a total length of 256 bytes. + + Properties: + .offset_table (OffsetTable or None, None if offset table was + written into the Start record instead) + .validation (Validation object) + """ + offset_table = None # type: OffsetTable or None + validation = None # type: Validation + + def __init__(self, + validation: Validation, + offset_table: OffsetTable = None): + """ + :param validation: Validation object for this file. + :param offset_table: OffsetTable, or None if the Start record + contained an OffsetTable. Default None. + """ + self.validation = validation + self.offset_table = offset_table + + def merge_with_modals(self, modals: Modals): + pass + + def deduplicate_with_modals(self, modals: Modals): + pass + + @staticmethod + def read(stream: io.BufferedIOBase, + record_id: int, + has_offset_table: bool + ) -> 'End': + if record_id != 2: + raise InvalidDataError('Invalid record id for End {}'.format(record_id)) + if has_offset_table: + offset_table = OffsetTable.read(stream) + else: + offset_table = None + _padding_string = read_bstring(stream) + validation = Validation.read(stream) + return End(validation, offset_table) + + def write(self, stream: io.BufferedIOBase) -> int: + size = write_uint(stream, 2) + if self.offset_table is not None: + size += self.offset_table.write(stream) + + buf = io.BytesIO() + self.validation.write(buf) + validation_bytes = buf.getvalue() + + pad_len = 256 - size - len(validation_bytes) + if pad_len > 0: + pad = [0x80] * (pad_len - 1) + [0x00] + stream.write(bytes(pad)) + stream.write(validation_bytes) + return 256 + + +class CBlock(Record): + """ + CBlock (Compressed Block) record (ID 34) + + Properties: + .compression_type (int, 0 for zlib) + .decompressed_byte_count (int) + .compressed_bytes (bytes) + """ + compression_type = None # type: int + decompressed_byte_count = None # type: int + compressed_bytes = None # type: bytes + + def __init__(self, + compression_type: int, + decompressed_byte_count: int, + compressed_bytes: bytes): + """ + :param compression_type: 0 (zlib) + :param decompressed_byte_count: Number of bytes in the decompressed data. + :param compressed_bytes: The compressed data. + """ + if compression_type != 0: + raise InvalidDataError('CBlock: Invalid compression scheme ' + '{}'.format(compression_type)) + + self.compression_type = compression_type + self.decompressed_byte_count = decompressed_byte_count + self.compressed_bytes = compressed_bytes + + def merge_with_modals(self, modals: Modals): + pass + + def deduplicate_with_modals(self, modals: Modals): + pass + + @staticmethod + def read(stream: io.BufferedIOBase, record_id: int) -> 'CBlock': + if record_id != 34: + raise InvalidDataError('Invalid record id for CBlock: ' + '{}'.format(record_id)) + compression_type = read_uint(stream) + decompressed_count = read_uint(stream) + compressed_bytes = read_bstring(stream) + return CBlock(compression_type, decompressed_count, compressed_bytes) + + def write(self, stream: io.BufferedIOBase) -> int: + size = write_uint(stream, 34) + size += write_uint(stream, self.compression_type) + size += write_uint(stream, self.decompressed_byte_count) + size += write_bstring(stream, self.compressed_bytes) + return size + + @staticmethod + def from_decompressed(decompressed_bytes: bytes, + compression_type: int = 0, + compression_args: Dict = None + ) -> 'CBlock': + """ + Create a CBlock record from uncompressed data. + + :param decompressed_bytes: Uncompressed data (one or more non-CBlock records) + :param compression_type: Compression type (0: zlib). Default 0 + :param compression_args Passed as kwargs to zlib.compressobj(). Default {}. + :return: CBlock object constructed from the data. + :raises: InvalidDataError if invalid compression_type. + """ + if compression_args is None: + compression_args = {} + + if compression_type == 0: + count = len(decompressed_bytes) + compressor = zlib.compressobj(wbits=-zlib.MAX_WBITS, **compression_args) + compressed_bytes = compressor.compress(decompressed_bytes) + \ + compressor.flush() + else: + raise InvalidDataError('Unknown compression type: ' + '{}'.format(compression_type)) + + return CBlock(compression_type, count, compressed_bytes) + + def decompress(self, decompression_args) -> bytes: + """ + Decompress the contents of this CBlock. + + :param decompression_args: Passed as kwargs to zlib.decompressobj(). + :return: Decompressed bytes object. + :raises: InvalidDataError if data is malformed or compression type is + unknonwn. + """ + if self.compression_type == 0: + decompressor = zlib.decompressobj(wbits=-zlib.MAX_WBITS, **decompression_args) + decompressed_bytes = decompressor.decompress(self.compressed_bytes) + \ + decompressor.flush() + if len(decompressed_bytes) != self.decompressed_byte_count: + raise InvalidDataError('Decompressed data length does not match!') + else: + raise InvalidDataError('Unknown compression type: ' + '{}'.format(self.compression_type)) + return decompressed_bytes + + +class CellName(Record): + """ + CellName record (ID 3, 4) + + Properties: + .nstring (NString) + .reference_number (int or None) + """ + nstring = None # type: NString + reference_number = None # type: int or None + + def __init__(self, + nstring: NString or str, + reference_number: int = None): + """ + :param nstring: The contained string. + :param reference_number: Reference id number for the string. + Default is to use an implicitly-assigned number. + """ + if isinstance(nstring, NString): + self.nstring = nstring + else: + self.nstring = NString(nstring) + self.reference_number = reference_number + + def merge_with_modals(self, modals: Modals): + modals.reset() + + def deduplicate_with_modals(self, modals: Modals): + modals.reset() + + @staticmethod + def read(stream: io.BufferedIOBase, record_id: int) -> 'CellName': + if record_id not in (3, 4): + raise InvalidDataError('Invalid record id for CellName ' + '{}'.format(record_id)) + nstring = NString.read(stream) + if record_id == 4: + reference_number = read_uint(stream) + else: + reference_number = None + return CellName(nstring, reference_number) + + def write(self, stream: io.BufferedIOBase) -> int: + record_id = 3 + (self.reference_number is not None) + size = write_uint(stream, record_id) + size += self.nstring.write(stream) + if self.reference_number is not None: + size += write_uint(stream, self.reference_number) + return size + +class PropName(Record): + """ + PropName record (ID 7, 8) + + Properties: + .nstring (NString) + .reference_number (int or None) + """ + nstring = None # type: NString + reference_number = None # type: int or None + + def __init__(self, + nstring: NString or str, + reference_number: int = None): + """ + :param nstring: The contained string. + :param reference_number: Reference id number for the string. + Default is to use an implicitly-assigned number. + """ + if isinstance(nstring, NString): + self.nstring = nstring + else: + self.nstring = NString(nstring) + self.reference_number = reference_number + + def merge_with_modals(self, modals: Modals): + modals.reset() + + def deduplicate_with_modals(self, modals: Modals): + modals.reset() + + @staticmethod + def read(stream: io.BufferedIOBase, record_id: int) -> 'PropName': + if record_id not in (7, 8): + raise InvalidDataError('Invalid record id for PropName ' + '{}'.format(record_id)) + nstring = NString.read(stream) + if record_id == 8: + reference_number = read_uint(stream) + else: + reference_number = None + return PropName(nstring, reference_number) + + def write(self, stream: io.BufferedIOBase) -> int: + record_id = 7 + (self.reference_number is not None) + size = write_uint(stream, record_id) + size += self.nstring.write(stream) + if self.reference_number is not None: + size += write_uint(stream, self.reference_number) + return size + + +class TextString(Record): + """ + TextString record (ID 5, 6) + + Properties: + .astring (AString) + .reference_number (int or None) + """ + astring = None # type: AString + reference_number = None # type: int or None + + def __init__(self, + string: AString or str, + reference_number: int = None): + """ + :param string: The contained string. + :param reference_number: Reference id number for the string. + Default is to use an implicitly-assigned number. + """ + if isinstance(string, AString): + self.astring = string + else: + self.astring = AString(string) + self.reference_number = reference_number + + def merge_with_modals(self, modals: Modals): + modals.reset() + + def deduplicate_with_modals(self, modals: Modals): + modals.reset() + + @staticmethod + def read(stream: io.BufferedIOBase, record_id: int) -> 'TextString': + if record_id not in (5, 6): + raise InvalidDataError('Invalid record id for TextString: ' + '{}'.format(record_id)) + astring = AString.read(stream) + if record_id == 6: + reference_number = read_uint(stream) + else: + reference_number = None + return TextString(astring, reference_number) + + def write(self, stream: io.BufferedIOBase) -> int: + record_id = 5 + (self.reference_number is not None) + size = write_uint(stream, record_id) + size += self.astring.write(stream) + if self.reference_number is not None: + size += write_uint(stream, self.reference_number) + return size + + +class PropString(Record): + """ + PropString record (ID 9, 10) + + Properties: + .astring (AString) + .reference_number (int or None) + """ + astring = None # type: AString + reference_number = None # type: int or None + + def __init__(self, + string: AString or str, + reference_number: int = None): + """ + :param string: The contained string. + :param reference_number: Reference id number for the string. + Default is to use an implicitly-assigned number. + """ + if isinstance(astring, AString): + self.astring = astring + else: + self.astring = AString(astring) + self.reference_number = reference_number + + def merge_with_modals(self, modals: Modals): + modals.reset() + + def deduplicate_with_modals(self, modals: Modals): + modals.reset() + + @staticmethod + def read(stream: io.BufferedIOBase, record_id: int) -> 'PropString': + if record_id not in (9, 10): + raise InvalidDataError('Invalid record id for PropString: ' + '{}'.format(record_id)) + astring = AString.read(stream) + if record_id == 10: + reference_number = read_uint(stream) + else: + reference_number = None + return PropString(astring, reference_number) + + def write(self, stream: io.BufferedIOBase) -> int: + record_id = 9 + (self.reference_number is not None) + size = write_uint(stream, record_id) + size += self.astring.write(stream) + if self.reference_number is not None: + size += write_uint(stream, self.reference_number) + return size + + +class LayerName(Record): + """ + LayerName record (ID 11, 12) + + Properties: + .nstring (NString) + .layer_interval (Tuple, (int or None, int or None), + bounds on the interval) + .type_interval (Tuple, (int or None, int or None), + bounds on the interval) + .is_textlayer (bool) + """ + nstring = None # type: NString, + layer_interval = None # type: Tuple + type_interval = None # type: Tuple + is_textlayer = None # type: bool + + def __init__(self, + nstring: NString or str, + layer_interval: Tuple, + type_interval: Tuple, + is_textlayer: bool): + """ + :param nstring: The layer name. + :param layer_interval: Tuple (int or None, int or None) giving bounds + (or lack of thereof) on the layer number. + :param type_interval: Tuple (int or None, int or None) giving bounds + (or lack of thereof) on the type number. + :param is_textlayer: True if the layer is a text layer. + """ + if isinstance(nstring, NString): + self.nstring = nstring + else: + self.nstring = NString(nstring) + self.layer_interval = layer_interval + self.type_interval = type_interval + self.is_textlayer = is_textlayer + + def merge_with_modals(self, modals: Modals): + modals.reset() + + def deduplicate_with_modals(self, modals: Modals): + modals.reset() + + @staticmethod + def read(stream: io.BufferedIOBase, record_id: int) -> 'LayerName': + if record_id not in (11, 12): + raise InvalidDataError('Invalid record id for LayerName: ' + '{}'.format(record_id)) + is_textlayer = (record_id == 12) + nstring = AString.read(stream) + layer_interval = read_interval(stream) + type_interval = read_interval(stream) + return LayerName(nstring, layer_interval, type_interval, is_textlayer) + + def write(self, stream: io.BufferedIOBase) -> int: + record_id = 11 + self.is_textlayer + size = write_uint(stream, record_id) + size += self.nstring.write(stream) + size += write_interval(stream, *self.layer_interval) + size += write_interval(stream, *self.type_interval) + return size + + +class Property(Record): + """ + LayerName record (ID 28, 29) + + Properties: + .name (NString or int or None, + int is an explicit reference, + None is a flag to use Modal) + .values (List of property values or None) + .is_standard (bool, whether this is a standard property) + """ + name = None # type: NString or int or None, + values = None # type: List[property_value_t] or None + is_standard = None # type: bool or None + + def __init__(self, + name: NString or str or int = None, + values: List[property_value_t] = None, + is_standard: bool = None): + """ + :param name: Property name, reference number, or None (i.e. use modal) + Default None. + :param values: List of property values, or None (i.e. use modal) + Default None. + :param is_standard: True if this is a standard property. None to use modal. + Default None. + """ + if isinstance(name, str): + self.name = NString(name) + else: + self.name = name + self.values = values + self.is_standard = is_standard + + def merge_with_modals(self, modals: Modals): + adjust_field(self, 'name', modals, 'property_name') + adjust_field(self, 'values', modals, 'property_value_list') + adjust_field(self, 'is_standard', modals, 'property_is_standard') + + def deduplicate_with_modals(self, modals: Modals): + dedup_field(self, 'name', modals, 'property_name') + dedup_field(self, 'values', modals, 'property_value_list') + if self.values is None and self.name is None: + dedup_field(self, 'is_standard', modals, 'property_is_standard') + + @staticmethod + def read(stream: io.BufferedIOBase, record_id: int) -> 'Property': + if record_id not in (28, 29): + raise InvalidDataError('Invalid record id for PropertyValue: ' + '{}'.format(record_id)) + if record_id == 29: + return Property() + else: + byte = read_byte(stream) #UUUUVCNS + u = 0x0f & (byte >> 4) + v = 0x01 & (byte >> 3) + c = 0x01 & (byte >> 2) + n = 0x01 & (byte >> 1) + s = 0x01 & (byte >> 0) + + name = read_refname(stream, c, n) + if v == 0: + if u < 0x0f: + value_count = u + else: + value_count = read_uint(stream) + values = [read_property_value(stream) for _ in range(value_count)] + else: + values = None + if u != 0: + raise InvalidDataError('Malformed property record header') + return Property(name, values, s) + + def write(self, stream: io.BufferedIOBase) -> int: + if self.is_standard is None and self.values is None and self.name is None: + return write_uint(stream, 29) + else: + if self.is_standard is None: + raise InvalidDataError('Property has value or name, ' + 'but no is_standard flag!') + if self.values is not None: + value_count = len(self.values) + v = 0 + if value_count >= 0x0f: + u = 0x0f + else: + u = value_count + else: + v = 1 + u = 0 + + c = self.name is not None + n = c and isinstance(self.name, int) + s = self.is_standard + + size = write_uint(stream, 28) + size += write_byte(stream, (u << 4) | (v << 3) | (c << 2) | (n << 1) | s) + if c: + if n: + size += write_uint(stream, self.name) + else: + size += self.name.write(stream) + if not v: + if u == 0x0f: + size += write_uint(stream, self.name) + size += sum(write_property_value(stream, p) for p in self.values) + return size + + +class XName(Record): + """ + XName record (ID 30, 31) + + Properties: + .attribute (int) + .bstring (bytes) + .reference_number (int or None, None means to use implicity numbering) + """ + attribute = None # type: int + bstring = None # type: bytes + reference_number = None # type: int or None + + def __init__(self, + attribute: int, + bstring: bytes, + reference_number: int = None): + """ + :param attribute: Attribute number. + :param bstring: Binary XName data. + :param reference_number: Reference number for this XName. + Default None (implicit). + """ + self.attribute = attribute + self.bstring = bstring + self.reference_number = reference_number + + def merge_with_modals(self, modals: Modals): + modals.reset() + + def deduplicate_with_modals(self, modals: Modals): + modals.reset() + + @staticmethod + def read(stream: io.BufferedIOBase, record_id: int) -> 'XName': + if record_id not in (30, 31): + raise InvalidDataError('Invalid record id for XName: ' + '{}'.format(record_id)) + attribute = read_uint(stream) + bstring = read_bstring(stream) + if record_id == 31: + reference_number = read_uint(stream) + else: + reference_number = None + return XName(attribute, bstring, reference_number) + + def write(self, stream: io.BufferedIOBase) -> int: + record_id = 30 + (self.reference_number is not None) + size = write_uint(stream, record_id) + size += write_uint(stream, self.attribute) + size += write_bstring(stream, self.bstring) + if self.reference_number is not None: + size += write_uint(stream, self.reference_number) + return size + + +class XElement(Record): + """ + XElement record (ID 32) + + Properties: + .attribute (int) + .bstring (bytes) + """ + attribute = None # type: int + bstring = None # type: bytes + + def __init__(self, attribute: int, bstring: bytes): + """ + :param attribute: Attribute number. + :param bstring: Binary data for this XElement. + """ + self.attribute = attribute + self.bstring = bstring + + def merge_with_modals(self, modals: Modals): + pass + + def deduplicate_with_modals(self, modals: Modals): + pass + + @staticmethod + def read(stream: io.BufferedIOBase, record_id: int) -> 'XElement': + if record_id != 32: + raise InvalidDataError('Invalid record id for XElement: ' + '{}'.format(record_id)) + attribute = read_uint(stream) + bstring = read_bstring(stream) + return XElement(attribute, bstring) + + def write(self, stream: io.BufferedIOBase) -> int: + size = write_uint(stream, 32) + size += write_uint(stream, self.attribute) + size += write_bstring(stream, self.bstring) + return size + + +class XGeometry(Record): + """ + XGeometry record (ID 33) + + Properties: + .attribute (int) + .bstring (bytes) + .layer (int or None, None means reuse modal) + .datatype (int or None, None means reuse modal) + .x (int or None, None means reuse modal) + .y (int or None, None means reuse modal) + .repetition (reptetition or None) + """ + attribute = None # type: int + bstring = None # type: bytes + layer = None # type: int or None + datatype = None # type: int or None + x = None # type: int or None + y = None # type: int or None + repetition = None # type: repetition_t or None + + def __init__(self, + attribute: int, + bstring: bytes, + layer: int = None, + datatype: int = None, + x: int = None, + y: int = None, + repetition: repetition_t = None): + """ + :param attribute: Attribute number for this XGeometry. + :param bstring: Binary data for this XGeometry. + :param layer: Layer number. Default None (reuse modal). + :param datatype: Datatype number. Default None (reuse modal). + :param x: X-offset. Default None (use modal). + :param y: Y-offset. Default None (use modal). + :param repetition: Repetition. Default None (no repetition). + """ + self.attribute = attribute + self.bstring = bstring + self.layer = layer + self.datatype = datatype + self.x = x + self.y = y + self.repetition = repetition + + def merge_with_modals(self, modals: Modals): + adjust_coordinates(self, modals, 'geometry_x', 'geometry_y') + adjust_repetition(self, modals) + adjust_field(self, 'layer', modals, 'layer') + adjust_field(self, 'datatype', modals, 'datatype') + + def deduplicate_with_modals(self, modals: Modals): + dedup_coordinates(self, modals, 'geometry_x', 'geometry_y') + dedup_repetition(self, modals) + dedup_field(self, 'layer', modals, 'layer') + dedup_field(self, 'datatype', modals, 'datatype') + + @staticmethod + def read(stream: io.BufferedIOBase, record_id: int) -> 'XGeometry': + if record_id != 33: + raise InvalidDataError('Invalid record id for XGeometry: ' + '{}'.format(record_id)) + + z0, z1, z2, x, y, r, d, l = read_bool_byte(stream) + if z0 or z1 or z2: + raise InvalidDataError('Malformed XGeometry header') + attribute = read_uint(stream) + optional = {} + if l: + optional['layer'] = read_uint(stream) + if d: + optional['datatype'] = read_uint(stream) + bstring = read_bstring(stream) + if x: + optional['x'] = read_sint(stream) + if y: + optional['y'] = read_sint(stream) + if r: + optional['repetition'] = read_repetition(stream) + + return XGeometry(attribute, bstring, **optional) + + def write(self, stream: io.BufferedIOBase) -> int: + x = self.x is not None + y = self.y is not None + r = self.repetition is not None + d = self.datatype is not None + l = self.layer is not None + + size = write_uint(stream, 33) + size += write_bool_byte(stream, (0, 0, 0, x, y, r, d, l)) + size += write_uint(stream, self.attribute) + if l: + size += write_uint(stream, self.layer) + if d: + size += write_uint(stream, self.datatype) + size += write_bstring(stream, self.bstring) + if x: + size += write_sint(stream, self.x) + if y: + size += write_sint(stream, self.y) + if r: + size += self.repetition.write(stream) + return size + + +class Cell(Record): + """ + Cell record (ID 13, 14) + + Properties: + .name (NString or int specifying CellName reference number) + """ + name = None # type: int or NString + + def __init__(self, name: int or NString): + """ + :param name: NString, or an int specifying a CellName reference number. + """ + self.name = name + + def merge_with_modals(self, modals: Modals): + modals.reset() + + def deduplicate_with_modals(self, modals: Modals): + modals.reset() + + @staticmethod + def read(stream: io.BufferedIOBase, record_id: int) -> 'Cell': + if record_id == 13: + name = read_uint(stream) + elif record_id == 14: + name = NString.read(stream) + else: + raise InvalidDataError('Invalid record id for Cell: ' + '{}'.format(record_id)) + return Cell(name) + + def write(self, stream: io.BufferedIOBase) -> int: + size = 0 + if isinstance(self.name, int): + size += write_uint(stream, 13) + size += write_uint(stream, self.name) + else: + size += write_uint(stream, 14) + size += self.name.write(stream) + return size + + +class Placement(Record): + """ + Placement record (ID 17, 18) + + Properties: + .attribute (int) + .name (NString, name or + int, CellName reference number or + None, reuse modal) + .magnification (real) + .angle (real, degrees counterclockwise) + .x (int or None, None means reuse modal) + .y (int or None, None means reuse modal) + .repetition (reptetition or None) + .flip (bool) + """ + name = None # type: NString or int or None + magnification = None # type: real_t or None + angle = None # type: real_t or None + x = None # type: int or None + y = None # type: int or None + repetition = None # type: repetition_t or None + flip = None # type: bool + + def __init__(self, + flip: bool, + name: NString or str or int = None, + magnification: real_t = None, + angle: real_t = None, + x: int = None, + y: int = None, + repetition: repetition_t = None): + """ + :param flip: Whether to perform reflection about the x-axis. + :param name: NString, an int specifying a CellName reference number, + or None (reuse modal). + :param magnification: Magnification factor. Default None (use modal). + :param angle: Rotation angle in degrees, counterclockwise. + Default None (reuse modal). + :param x: X-offset. Default None (use modal). + :param y: Y-offset. Default None (use modal). + :param repetition: Repetition. Default None (no repetition). + """ + self.x = x + self.y = y + self.repetition = repetition + self.flip = flip + self.magnification = magnification + self.angle = angle + if isinstance(name, str): + self.name = NString(name) + else: + self.name = name + + def merge_with_modals(self, modals: Modals): + adjust_coordinates(self, modals, 'placement_x', 'placement_y') + adjust_repetition(self, modals) + adjust_field(self, 'name', modals, 'placement_cell') + + def deduplicate_with_modals(self, modals: Modals): + dedup_coordinates(self, modals, 'placement_x', 'placement_y') + dedup_repetition(self, modals) + dedup_field(self, 'name', modals, 'placement_cell') + + @staticmethod + def read(stream: io.BufferedIOBase, record_id: int) -> 'Placement': + if record_id not in (17, 18): + raise InvalidDataError('Invalid record id for Placement: ' + '{}'.format(record_id)) + + #CNXYRAAF (17) or CNXYRMAF (18) + c, n, x, y, r, ma0, ma1, flip = read_bool_byte(stream) + + optional = {} + name = read_refname(stream, c, n) + if record_id == 17: + aa = (ma0 << 1) | ma1 + optional['angle'] = aa * 90 + elif record_id == 18: + m = ma0 + a = ma1 + if m: + optional['magnification'] = read_real(stream) + if a: + optional['angle'] = read_real(stream) + if x: + optional['x'] = read_sint(stream) + if y: + optional['y'] = read_sint(stream) + if r: + optional['repetition'] = read_repetition(stream) + + return Placement(flip, name, **optional) + + def write(self, stream: io.BufferedIOBase) -> int: + c = self.name is not None + n = c and isinstance(self.name, int) + x = self.x is not None + y = self.y is not None + r = self.repetition is not None + f = self.flip + + if self.angle is not None and self.angle % 90 == 0 and \ + self.magnification is None or self.magnification == 1: + aa = int((self.angle / 90) % 4) + bools = (c, n, x, y, r, aa & 0b10, aa & 0b01, f) + m = False + a = False + record_id = 17 + else: + m = self.magnification is not None + a = self.angle is not None + bools = (c, n, x, y, r, m, a, f) + record_id = 18 + + size = write_uint(stream, record_id) + size += write_bool_byte(stream, bools) + if c: + if n: + size += write_uint(stream, self.name) + else: + size += self.name.write(self) + if m: + size += write_real(stream, self.magnification) + if a: + size += write_real(stream, self.angle) + if x: + size += write_sint(stream, self.x) + if y: + size += write_sint(stream, self.y) + if r: + size += self.repetition.write(stream) + return size + + +class Text(Record): + """ + Text record (ID 19) + + Properties: + .string (AString or int or None, None means reuse modal) + .layer (int or None, None means reuse modal) + .datatype (int or None, None means reuse modal) + .x (int or None, None means reuse modal) + .y (int or None, None means reuse modal) + .repetition (reptetition or None) + """ + string = None # type: AString or int or None + layer = None # type: int or None + datatype = None # type: int or None + x = None # type: int or None + y = None # type: int or None + repetition = None # type: repetition_t or None + + def __init__(self, + string: AString or str or int = None, + layer: int = None, + datatype: int = None, + x: int = None, + y: int = None, + repetition: repetition_t = None): + """ + :param string: Text content, or TextString reference number. + Default None (use modal). + :param layer: Layer number. Default None (reuse modal). + :param datatype: Datatype number. Default None (reuse modal). + :param x: X-offset. Default None (use modal). + :param y: Y-offset. Default None (use modal). + :param repetition: Repetition. Default None (no repetition). + """ + self.layer = layer + self.datatype = datatype + self.x = x + self.y = y + self.repetition = repetition + if isinstance(string, str): + self.string = AString(string) + else: + self.string = string + + def merge_with_modals(self, modals: Modals): + adjust_coordinates(self, modals, 'text_x', 'text_y') + adjust_repetition(self, modals) + adjust_field(self, 'string', modals, 'text_string') + adjust_field(self, 'layer', modals, 'text_layer') + adjust_field(self, 'datatype', modals, 'text_datatype') + + def deduplicate_with_modals(self, modals: Modals): + dedup_coordinates(self, modals, 'text_x', 'text_y') + dedup_repetition(self, modals) + dedup_field(self, 'string', modals, 'text_string') + dedup_field(self, 'layer', modals, 'text_layer') + dedup_field(self, 'datatype', modals, 'text_datatype') + + @staticmethod + def read(stream: io.BufferedIOBase, record_id: int) -> 'Text': + if record_id != 19: + raise InvalidDataError('Invalid record id for Text: ' + '{}'.format(record_id)) + + z0, c, n, x, y, r, d, l = read_bool_byte(stream) + if z0: + raise InvalidDataError('Malformed Text header') + + optional = {} + string = read_refstring(stream, c, n) + if l: + optional['layer'] = read_uint(stream) + if d: + optional['datatype'] = read_uint(stream) + if x: + optional['x'] = read_sint(stream) + if y: + optional['y'] = read_sint(stream) + if r: + optional['repetition'] = read_repetition(stream) + + return Text(string, **optional) + + def write(self, stream: io.BufferedIOBase) -> int: + c = self.string is not None + n = c and isinstance(self.string, int) + x = self.x is not None + y = self.y is not None + r = self.repetition is not None + d = self.datatype is not None + l = self.layer is not None + + size = write_uint(stream, 19) + size += write_bool_byte(stream, (0, c, n, x, y, r, d, l)) + if c: + if n: + size += write_uint(stream, self.string) + else: + size += self.string.write(self) + if l: + size += write_uint(stream, self.layer) + if d: + size += write_uint(stream, self.datatype) + if x: + size += write_sint(stream, self.x) + if y: + size += write_sint(stream, self.y) + if r: + size += self.repetition.write(stream) + return size + + +class Rectangle(Record): + """ + Rectangle record (ID 20) + + Properties: + .is_square (bool, True if this is a square. + If True, height must be None.) + .width (int or None, None means reuse modal) + .height (int or None, Must be None if .is_square is True. + If .is_square is False, None means reuse modal) + .layer (int or None, None means reuse modal) + .datatype (int or None, None means reuse modal) + .x (int or None, None means use modal) + .y (int or None, None means use modal) + .repetition (reptetition or None) + """ + layer = None # type: int or None + datatype = None # type: int or None + width = None # type: int or None + height = None # type: int or None + x = None # type: int or None + y = None # type: int or None + repetition = None # type: repetition_t or None + is_square = None # type: bool + + def __init__(self, + is_square: bool = False, + layer: int = None, + datatype: int = None, + width: int = None, + height: int = None, + x: int = None, + y: int = None, + repetition: repetition_t = None): + """ + :param is_square: True if this is a square. If True, height must + be None. Default False. + :param layer: Layer number. Default None (reuse modal). + :param datatype: Datatype number. Default None (reuse modal). + :param width: X-width. Default None (reuse modal). + :param height: Y-height. Default None (reuse modal, or use width if + square). Must be None if is_square is True. + :param x: X-offset. Default None (use modal). + :param y: Y-offset. Default None (use modal). + :param repetition: Repetition. Default None (no repetition). + """ + self.is_square = is_square + self.layer = layer + self.datatype = datatype + self.width = width + self.height = height + self.x = x + self.y = y + self.repetition = repetition + if is_square and self.height is not None: + raise InvalidDataError('Rectangle is square and also has height') + + def merge_with_modals(self, modals: Modals): + adjust_coordinates(self, modals, 'geometry_x', 'geometry_y') + adjust_repetition(self, modals) + adjust_field(self, 'layer', modals, 'layer') + adjust_field(self, 'datatype', modals, 'datatype') + adjust_field(self, 'width', modals, 'geometry_w') + if self.is_square: + adjust_field(self, 'width', modals, 'geometry_h') + else: + adjust_field(self, 'height', modals, 'geometry_h') + + def deduplicate_with_modals(self, modals: Modals): + dedup_coordinates(self, modals, 'geometry_x', 'geometry_y') + dedup_repetition(self, modals) + dedup_field(self, 'layer', modals, 'layer') + dedup_field(self, 'datatype', modals, 'datatype') + dedup_field(self, 'width', modals, 'geometry_w') + if self.is_square: + dedup_field(self, 'width', modals, 'geometry_h') + else: + dedup_field(self, 'height', modals, 'geometry_h') + + @staticmethod + def read(stream: io.BufferedIOBase, record_id: int) -> 'Rectangle': + if record_id != 20: + raise InvalidDataError('Invalid record id for Rectangle: ' + '{}'.format(record_id)) + + is_square, w, h, x, y, r, d, l = read_bool_byte(stream) + optional = {} + if l: + optional['layer'] = read_uint(stream) + if d: + optional['datatype'] = read_uint(stream) + if w: + optional['width'] = read_uint(stream) + if h: + optional['height'] = read_uint(stream) + if x: + optional['x'] = read_sint(stream) + if y: + optional['y'] = read_sint(stream) + if r: + optional['repetition'] = read_repetition(stream) + return Rectangle(is_square, **optional) + + def write(self, stream: io.BufferedIOBase) -> int: + s = self.is_square + w = self.width is not None + h = self.height is not None + x = self.x is not None + y = self.y is not None + r = self.repetition is not None + d = self.datatype is not None + l = self.layer is not None + + size = write_uint(stream, 20) + size += write_bool_byte(stream, (s, w, h, x, y, r, d, l)) + if l: + size += write_uint(stream, self.layer) + if d: + size += write_uint(stream, self.datatype) + if w: + size += write_uint(stream, self.width) + if h: + size += write_uint(stream, self.height) + if x: + size += write_sint(stream, self.x) + if y: + size += write_sint(stream, self.y) + if r: + size += self.repetition.write(stream) + return size + + +class Polygon(Record): + """ + Polygon record (ID 21) + + Properties: + .point_list ([[x0, y0], [x1, y1], ...] or None, + list is an implicitly closed path, + vertices are [int, int], + None means reuse modal) + .layer (int or None, None means reuse modal) + .datatype (int or None, None means reuse modal) + .x (int or None, None means reuse modal) + .y (int or None, None means reuse modal) + .repetition (reptetition or None) + """ + layer = None # type: int or None + datatype = None # type: int or None + x = None # type: int or None + y = None # type: int or None + repetition = None # type: repetition_t or None + point_list = None # type: List[List[int]] or None + + def __init__(self, + point_list: List[List[int]] = None, + layer: int = None, + datatype: int = None, + x: int = None, + y: int = None, + repetition: repetition_t = None): + """ + :param point_list: List of vertices [[x0, y0], [x1, y1], ...]. + List forms an implicitly closed path + Default None (reuse modal). + :param layer: Layer number. Default None (reuse modal). + :param datatype: Datatype number. Default None (reuse modal). + :param x: X-offset. Default None (use modal). + :param y: Y-offset. Default None (use modal). + :param repetition: Repetition. Default None (no repetition). + """ + self.layer = layer + self.datatype = datatype + self.x = x + self.y = y + self.repetition = repetition + self.point_list = point_list + + if point_list is not None: + if len(point_list) < 3: + raise InvalidDataError('Polygon with < 3 points') + + def merge_with_modals(self, modals: Modals): + adjust_coordinates(self, modals, 'geometry_x', 'geometry_y') + adjust_repetition(self, modals) + adjust_field(self, 'layer', modals, 'layer') + adjust_field(self, 'datatype', modals, 'datatype') + adjust_field(self, 'point_list', modals, 'polygon_point_list') + + def deduplicate_with_modals(self, modals: Modals): + dedup_coordinates(self, modals, 'geometry_x', 'geometry_y') + dedup_repetition(self, modals) + dedup_field(self, 'layer', modals, 'layer') + dedup_field(self, 'datatype', modals, 'datatype') + dedup_field(self, 'point_list', modals, 'polygon_point_list') + + @staticmethod + def read(stream: io.BufferedIOBase, record_id: int) -> 'Polygon': + if record_id != 21: + raise InvalidDataError('Invalid record id for Polygon: ' + '{}'.format(record_id)) + + z0, z1, p, x, y, r, d, l = read_bool_byte(stream) + if z0 or z1: + raise InvalidDataError('Invalid polygon header') + + optional = {} + if l: + optional['layer'] = read_uint(stream) + if d: + optional['datatype'] = read_uint(stream) + if p: + optional['point_list'] = read_point_list(stream) + if x: + optional['x'] = read_sint(stream) + if y: + optional['y'] = read_sint(stream) + if r: + optional['repetition'] = read_repetition(stream) + return Polygon(**optional) + + def write(self, stream: io.BufferedIOBase, fast: bool = False) -> int: + p = self.point_list is not None + x = self.x is not None + y = self.y is not None + r = self.repetition is not None + d = self.datatype is not None + l = self.layer is not None + + size = write_uint(stream, 21) + size += write_bool_byte(stream, (0, 0, p, x, y, r, d, l)) + if l: + size += write_uint(stream, self.layer) + if d: + size += write_uint(stream, self.datatype) + if p: + size += write_point_list(stream, self.point_list, implicit_closed=True, fast=fast) + if x: + size += write_sint(stream, self.x) + if y: + size += write_sint(stream, self.y) + if r: + size += self.repetition.write(stream) + return size + + +class Path(Record): + """ + Polygon record (ID 22) + + Properties: + .point_list ([[x0, y0], [x1, y1], ...] or None, + vertices are [int, int], + None means reuse modal) + .half_width (int or None, None means reuse modal) + .extension_start (Tuple or None, + None means reuse modal, + Tuple is of the form + (PathExtensionScheme, int or None) + second value is None unless using + PathExtensionScheme.Arbitrary + Value determines extension past start point. + .extension_end Same form as extension_end. Value determines + extension past end point. + .layer (int or None, None means reuse modal) + .datatype (int or None, None means reuse modal) + .x (int or None, None means use modal) + .y (int or None, None means use modal) + .repetition (reptetition or None) + """ + layer = None # type: int or None + datatype = None # type: int or None + x = None # type: int or None + y = None # type: int or None + repetition = None # type: repetition_t or None + point_list = None # type: List[List[int]] or None + half_width = None # type: int or None + extension_start = None # type: pathextension_t or None + extension_end = None # type: pathextension_t or None + + def __init__(self, + point_list: List[List[int]] = None, + half_width: int = None, + extension_start: pathextension_t = None, + extension_end: pathextension_t = None, + layer: int = None, + datatype: int = None, + x: int = None, + y: int = None, + repetition: repetition_t = None): + """ + :param point_list: List of vertices [[x0, y0], [x1, y1], ...]. + Default None (reuse modal). + :param half_width: Half-width of the path. Default None (reuse modal). + :param extension_start: Specification for path extension at start of path. + None or Tuple: (PathExtensionScheme, int or None). + int is used only for PathExtensionScheme.Arbitrary. + Default None (reuse modal). + :param extension_end: Specification for path extension at end of path. + None or Tuple: (PathExtensionScheme, int or None). + int is used only for PathExtensionScheme.Arbitrary. + Default None (reuse modal). + :param layer: Layer number. Default None (reuse modal). + :param datatype: Datatype number. Default None (reuse modal). + :param x: X-offset. Default None (use modal). + :param y: Y-offset. Default None (use modal). + :param repetition: Repetition. Default None (no repetition). + """ + self.layer = layer + self.datatype = datatype + self.x = x + self.y = y + self.repetition = repetition + self.point_list = point_list + self.half_width = half_width + self.extension_start = extension_start + self.extension_end = extension_end + + def merge_with_modals(self, modals: Modals): + adjust_coordinates(self, modals, 'geometry_x', 'geometry_y') + adjust_repetition(self, modals) + adjust_field(self, 'layer', modals, 'layer') + adjust_field(self, 'datatype', modals, 'datatype') + adjust_field(self, 'point_list', modals, 'path_point_list') + adjust_field(self, 'half_width', modals, 'path_half_width') + adjust_field(self, 'extension_start', modals, 'path_extension_start') + adjust_field(self, 'extension_end', modals, 'path_extension_end') + + def deduplicate_with_modals(self, modals: Modals): + dedup_coordinates(self, modals, 'geometry_x', 'geometry_y') + dedup_repetition(self, modals) + dedup_field(self, 'layer', modals, 'layer') + dedup_field(self, 'datatype', modals, 'datatype') + dedup_field(self, 'point_list', modals, 'path_point_list') + dedup_field(self, 'half_width', modals, 'path_half_width') + dedup_field(self, 'extension_start', modals, 'path_extension_start') + dedup_field(self, 'extension_end', modals, 'path_extension_end') + + @staticmethod + def read(stream: io.BufferedIOBase, record_id: int) -> 'Path': + if record_id != 22: + raise InvalidDataError('Invalid record id for Path: ' + '{}'.format(record_id)) + + e, w, p, x, y, r, d, l = read_bool_byte(stream) + optional = {} + if l: + optional['layer'] = read_uint(stream) + if d: + optional['datatype'] = read_uint(stream) + if w: + optional['half_width'] = read_uint(stream) + if e: + scheme = read_uint(stream) + scheme_end = scheme & 0b11 + scheme_start = (scheme >> 2) & 0b11 + + def get_pathext(ext_scheme: int) -> pathextension_t: + if ext_scheme == 0: + return None + elif ext_scheme == 1: + return PathExtensionScheme.Flush, None + elif ext_scheme == 2: + return PathExtensionScheme.HalfWidth, None + elif ext_scheme == 3: + return PathExtensionScheme.Arbitrary, read_sint(stream) + + optional['extension_start'] = get_pathext(scheme_start) + optional['extension_end'] = get_pathext(scheme_end) + if p: + optional['point_list'] = read_point_list(stream) + if x: + optional['x'] = read_sint(stream) + if y: + optional['y'] = read_sint(stream) + if r: + optional['repetition'] = read_repetition(stream) + return Polygon(**optional) + + def write(self, stream: io.BufferedIOBase, fast: bool = False) -> int: + e = self.extension_start is not None or self.extension_end is not None + w = self.half_width is not None + p = self.point_list is not None + x = self.x is not None + y = self.y is not None + r = self.repetition is not None + d = self.datatype is not None + l = self.layer is not None + + size = write_uint(stream, 21) + size += write_bool_byte(stream, (e, w, p, x, y, r, d, l)) + if l: + size += write_uint(stream, self.layer) + if d: + size += write_uint(stream, self.datatype) + if w: + size += write_uint(stream, self.half_width) + if e: + scheme = 0 + if self.extension_start is not None: + scheme += self.extension_start[0].value << 2 + if self.extension_end is not None: + scheme += self.extension_end[0].value + size += write_uint(stream, scheme) + if scheme & 0b1100 == 0b1100: + size += write_sint(stream, self.extension_start[1]) + if scheme & 0b0011 == 0b0011: + size += write_sint(stream, self.extension_end[1]) + if p: + size += write_point_list(stream, self.point_list, implicit_closed=False, fast=fast) + if x: + size += write_sint(stream, self.x) + if y: + size += write_sint(stream, self.y) + if r: + size += self.repetition.write(stream) + return size + + +class Trapezoid(Record): + """ + Trapezoid record (ID 23, 24, 25) + + Properties: + .delta_a (int or None, + If horizontal, signed x-distance from top left + vertex to bottom left vertex. If vertical, signed + y-distance from bottom left vertex to bottom right + vertex. + None means reuse modal.) + .delta_b (int or None, + If horizontal, signed x-distance from bottom right + vertex to top right vertex. If vertical, signed + y-distance from top right vertex to top left vertex. + None means reuse modal.) + .is_vertical (bool, True if the left and right sides are aligned to + the y-axis. If the trapezoid is a rectangle, either + True or False can be used.) + .width (int or None, Bounding box x-width, None means reuse modal) + .height (int or None, Bounding box y-height, None means reuse modal) + .layer (int or None, None means reuse modal) + .datatype (int or None, None means reuse modal) + .x (int or None, None means se modal) + .y (int or None, None means se modal) + .repetition (reptetition or None) + """ + layer = None # type: int or None + datatype = None # type: int or None + width = None # type: int or None + height = None # type: int or None + x = None # type: int or None + y = None # type: int or None + repetition = None # type: repetition_t or None + delta_a = None # type: int + delta_b = None # type: int + is_vertical = None # type: bool + + def __init__(self, + is_vertical: bool, + delta_a: int = 0, + delta_b: int = 0, + layer: int = None, + datatype: int = None, + width: int = None, + height: int = None, + x: int = None, + y: int = None, + repetition: repetition_t = None): + """ + :param is_vertical: True if both the left and right sides are aligned + to the y-axis. If the trapezoid is a rectangle, either value + is permitted. + :param delta_a: If horizontal, signed x-distance from top-left vertex + to bottom-left vertex. If vertical, signed y-distance from bottom- + left vertex to bottom-right vertex. Default None (reuse modal). + :param delta_b: If horizontal, signed x-distance from bottom-right vertex + to top right vertex. If vertical, signed y-distance from top-right + vertex to top-left vertex. Default None (reuse modal). + :param layer: Layer number. Default None (reuse modal). + :param datatype: Datatype number. Default None (reuse modal). + :param width: X-width of bounding box. Default None (reuse modal). + :param height: Y-height of bounding box. Default None (reuse modal) + :param x: X-offset. Default None (use modal). + :param y: Y-offset. Default None (use modal). + :param repetition: Repetition. Default None (no repetition). + :raises: InvalidDataError if dimensions are impossible. + """ + self.is_vertical = is_vertical + self.delta_a = delta_a + self.delta_b = delta_b + self.layer = layer + self.datatype = datatype + self.width = width + self.height = height + self.x = x + self.y = y + self.repetition = repetition + + if self.is_vertical: + if height is not None and delta_a + delta_b > height: + raise InvalidDataError('Trapezoid: h < delta_a + delta_b' + ' ({} < {} + {})'.format(height, delta_a, delta_b)) + else: + if width is not None and delta_a + delta_b > width: + raise InvalidDataError('Trapezoid: w < delta_a + delta_b' + ' ({} < {} + {})'.format(width, delta_a, delta_b)) + + def merge_with_modals(self, modals: Modals): + adjust_coordinates(self, modals, 'geometry_x', 'geometry_y') + adjust_repetition(self, modals) + adjust_field(self, 'layer', modals, 'layer') + adjust_field(self, 'datatype', modals, 'datatype') + adjust_field(self, 'width', modals, 'geometry_w') + adjust_field(self, 'height', modals, 'geometry_h') + + def deduplicate_with_modals(self, modals: Modals): + dedup_coordinates(self, modals, 'geometry_x', 'geometry_y') + dedup_repetition(self, modals) + dedup_field(self, 'layer', modals, 'layer') + dedup_field(self, 'datatype', modals, 'datatype') + dedup_field(self, 'width', modals, 'geometry_w') + dedup_field(self, 'height', modals, 'geometry_h') + + @staticmethod + def read(stream: io.BufferedIOBase, record_id: int) -> 'Trapezoid': + if record_id not in (23, 24, 25): + raise InvalidDataError('Invalid record id for Trapezoid: ' + '{}'.format(record_id)) + + is_vertical, w, h, x, y, r, d, l = read_bool_byte(stream) + optional = {} + if l: + optional['layer'] = read_uint(stream) + if d: + optional['datatype'] = read_uint(stream) + if w: + optional['width'] = read_uint(stream) + if h: + optional['height'] = read_uint(stream) + if record_id != 25: + optional['delta_a'] = read_sint(stream) + if record_id != 24: + optional['delta_b'] = read_sint(stream) + if x: + optional['x'] = read_sint(stream) + if y: + optional['y'] = read_sint(stream) + if r: + optional['repetition'] = read_repetition(stream) + return Trapezoid(is_vertical, **optional) + + def write(self, stream: io.BufferedIOBase) -> int: + v = self.is_vertical + w = self.width is not None + h = self.height is not None + x = self.x is not None + y = self.y is not None + r = self.repetition is not None + d = self.datatype is not None + l = self.layer is not None + + if self.delta_b == 0: + record_id = 24 + elif self.delta_a == 0: + record_id = 25 + else: + record_id = 23 + size = write_uint(stream, record_id) + size += write_bool_byte(stream, (v, w, h, x, y, r, d, l)) + if l: + size += write_uint(stream, self.layer) + if d: + size += write_uint(stream, self.datatype) + if w: + size += write_uint(stream, self.width) + if h: + size += write_uint(stream, self.height) + if record_id != 25: + size += write_sint(stream, self.delta_a) + if record_id != 24: + size += write_sint(stream, self.delta_b) + if x: + size += write_sint(stream, self.x) + if y: + size += write_sint(stream, self.y) + if r: + size += self.repetition.write(stream) + return size + + +# TODO: CTrapezoid type descriptions +class CTrapezoid(Record): + """ + CTrapezoid record (ID 26) + + Properties: + .ctrapezoid_type (int or None, see OASIS spec for details, None means reuse modal) + .width (int or None, Bounding box x-width, None means reuse modal) + .height (int or None, Bounding box y-height, None means reuse modal) + .layer (int or None, None means reuse modal) + .datatype (int or None, None means reuse modal) + .x (int or None, None means se modal) + .y (int or None, None means se modal) + .repetition (reptetition or None) + """ + ctrapezoid_type = None # type: int or None + layer = None # type: int or None + datatype = None # type: int or None + width = None # type: int or None + height = None # type: int or None + x = None # type: int or None + y = None # type: int or None + repetition = None # type: repetition_t or None + + def __init__(self, + ctrapezoid_type: int = None, + layer: int = None, + datatype: int = None, + width: int = None, + height: int = None, + x: int = None, + y: int = None, + repetition: repetition_t = None): + """ + :param ctrapezoid_type: CTrapezoid type; see OASIS format + documentation. Default None (reuse modal). + :param layer: Layer number. Default None (reuse modal). + :param datatype: Datatype number. Default None (reuse modal). + :param width: X-width of bounding box. Default None (reuse modal). + :param height: Y-height of bounding box. Default None (reuse modal) + :param x: X-offset. Default None (use modal). + :param y: Y-offset. Default None (use modal). + :param repetition: Repetition. Default None (no repetition). + :raises: InvalidDataError if dimensions are invalid. + """ + self.ctrapezoid_type = ctrapezoid_type + self.layer = layer + self.datatype = datatype + self.width = width + self.height = height + self.x = x + self.y = y + self.repetition = repetition + + if ctrapezoid_type in (20, 21) and width is not None: + raise InvalidDataError('CTrapezoid has spurious width entry: ' + '{}'.format(width)) + if ctrapezoid_type in (16, 17, 18, 19, 22, 23, 25) and height is not None: + raise InvalidDataError('CTrapezoid has spurious height entry: ' + '{}'.format(height)) + if ctrapezoid_type in range(0, 4) and width < height: + raise InvalidDataError('CTrapezoid has width < height' + ' ({} < {})'.format(width, height)) + if ctrapezoid_type in range(4, 8) and width < 2 * height: + raise InvalidDataError('CTrapezoid has width < 2*height' + ' ({} < 2 * {})'.format(width, height)) + if ctrapezoid_type in range(8, 12) and width > height: + raise InvalidDataError('CTrapezoid has width > height' + ' ({} > {})'.format(width, height)) + if ctrapezoid_type in range(12, 16) and 2 * width > height: + raise InvalidDataError('CTrapezoid has 2*width > height' + ' ({} > 2 * {})'.format(width, height)) + if ctrapezoid_type not in range(0, 26): + raise InvalidDataError('CTrapezoid has invalid type: ' + '{}'.format(ctrapezoid_type)) + + def merge_with_modals(self, modals: Modals): + adjust_coordinates(self, modals, 'geometry_x', 'geometry_y') + adjust_repetition(self, modals) + adjust_field(self, 'layer', modals, 'layer') + adjust_field(self, 'datatype', modals, 'datatype') + adjust_field(self, 'ctrapezoid_type', modals, 'ctrapezoid_type') + + if self.ctrapezoid_type in (20, 21): + if self.width is not None: + raise InvalidDataError('CTrapezoid has spurious width entry: ' + '{}'.format(self.width)) + else: + adjust_field(self, 'width', modals, 'geometry_w') + + if self.ctrapezoid_type in (16, 17, 18, 19, 22, 23, 25): + if self.height is not None: + raise InvalidDataError('CTrapezoid has spurious height entry: ' + '{}'.format(self.height)) + else: + adjust_field(self, 'height', modals, 'geometry_h') + + def deduplicate_with_modals(self, modals: Modals): + dedup_coordinates(self, modals, 'geometry_x', 'geometry_y') + dedup_repetition(self, modals) + dedup_field(self, 'layer', modals, 'layer') + dedup_field(self, 'datatype', modals, 'datatype') + dedup_field(self, 'width', modals, 'geometry_w') + dedup_field(self, 'height', modals, 'geometry_h') + dedup_field(self, 'ctrapezoid_type', modals, 'ctrapezoid_type') + + if self.ctrapezoid_type in (20, 21): + if self.width is not None: + raise InvalidDataError('CTrapezoid has spurious width entry: ' + '{}'.format(self.width)) + else: + dedup_field(self, 'width', modals, 'geometry_w') + + if self.ctrapezoid_type in (16, 17, 18, 19, 22, 23, 25): + if self.height is not None: + raise InvalidDataError('CTrapezoid has spurious height entry: ' + '{}'.format(self.height)) + else: + dedup_field(self, 'height', modals, 'geometry_h') + + @staticmethod + def read(stream: io.BufferedIOBase, record_id: int) -> 'CTrapezoid': + if record_id != 26: + raise InvalidDataError('Invalid record id for CTrapezoid: ' + '{}'.format(record_id)) + + t, w, h, x, y, r, d, l = read_bool_byte(stream) + optional = {} + if l: + optional['layer'] = read_uint(stream) + if d: + optional['datatype'] = read_uint(stream) + if t: + optional['ctrapezoid_type'] = read_uint(stream) + if w: + optional['width'] = read_uint(stream) + if h: + optional['height'] = read_uint(stream) + if x: + optional['x'] = read_sint(stream) + if y: + optional['y'] = read_sint(stream) + if r: + optional['repetition'] = read_repetition(stream) + return CTrapezoid(**optional) + + def write(self, stream: io.BufferedIOBase) -> int: + t = self.ctrapezoid_type is not None + w = self.width is not None + h = self.height is not None + x = self.x is not None + y = self.y is not None + r = self.repetition is not None + d = self.datatype is not None + l = self.layer is not None + + size = write_uint(stream, 26) + size += write_bool_byte(stream, (t, w, h, x, y, r, d, l)) + if l: + size += write_uint(stream, self.layer) + if d: + size += write_uint(stream, self.datatype) + if t: + size += write_uint(stream, self.ctrapezoid_type) + if w: + size += write_uint(stream, self.width) + if h: + size += write_uint(stream, self.height) + if x: + size += write_sint(stream, self.x) + if y: + size += write_sint(stream, self.y) + if r: + size += self.repetition.write(stream) + return size + + +class Circle(Record): + """ + Circle record (ID 27) + + Properties: + .radius (int or None, None means reuse modal) + .layer (int or None, None means reuse modal) + .datatype (int or None, None means reuse modal) + .x (int or None, None means se modal) + .y (int or None, None means se modal) + .repetition (reptetition or None) + """ + layer = None # type: int or None + datatype = None # type: int or None + x = None # type: int or None + y = None # type: int or None + repetition = None # type: repetition_t or None + radius = None # type: int or None + + def __init__(self, + radius: int = None, + layer: int = None, + datatype: int = None, + x: int = None, + y: int = None, + repetition: repetition_t = None): + """ + :param radius: Radius. Default None (reuse modal). + :param layer: Layer number. Default None (reuse modal). + :param datatype: Datatype number. Default None (reuse modal). + :param x: X-offset. Default None (use modal). + :param y: Y-offset. Default None (use modal). + :param repetition: Repetition. Default None (no repetition). + :raises: InvalidDataError if dimensions are invalid. + """ + self.radius = radius + self.layer = layer + self.datatype = datatype + self.x = x + self.y = y + self.repetition = repetition + + def merge_with_modals(self, modals: Modals): + adjust_coordinates(self, modals, 'geometry_x', 'geometry_y') + adjust_repetition(self, modals) + adjust_field(self, 'layer', modals, 'layer') + adjust_field(self, 'datatype', modals, 'datatype') + adjust_field(self, 'radius', modals, 'circle_radius') + + def deduplicate_with_modals(self, modals: Modals): + dedup_coordinates(self, modals, 'geometry_x', 'geometry_y') + dedup_repetition(self, modals) + dedup_field(self, 'layer', modals, 'layer') + dedup_field(self, 'datatype', modals, 'datatype') + dedup_field(self, 'radius', modals, 'circle_radius') + + @staticmethod + def read(stream: io.BufferedIOBase, record_id: int) -> 'Circle': + if record_id == 27: + raise InvalidDataError('Invalid record id for Circle: ' + '{}'.format(record_id)) + + z0, z1, has_radius, x, y, r, d, l = read_bool_byte(stream) + if z0 or z1: + raise InvalidDataError('Malformed circle header') + + optional = {} + if l: + optional['layer'] = read_uint(stream) + if d: + optional['datatype'] = read_uint(stream) + if has_radius: + optional['radius'] = read_uint(stream) + if x: + optional['x'] = read_sint(stream) + if y: + optional['y'] = read_sint(stream) + if r: + optional['repetition'] = read_repetition(stream) + return Circle(**optional) + + def write(self, stream: io.BufferedIOBase) -> int: + s = self.radius is not None + x = self.x is not None + y = self.y is not None + r = self.repetition is not None + d = self.datatype is not None + l = self.layer is not None + + size = write_uint(stream, 27) + size += write_bool_byte(stream, (0, 0, s, x, y, r, d, l)) + if l: + size += write_uint(stream, self.layer) + if d: + size += write_uint(stream, self.datatype) + if s: + size += write_uint(stream, self.radius) + if x: + size += write_sint(stream, self.x) + if y: + size += write_sint(stream, self.y) + if r: + size += self.repetition.write(stream) + return size + + +def adjust_repetition(record: Record, modals: Modals): + """ + Merge the record's repetition entry with the one in the modals + + :param record: Record to read or modify. + :param modals: Modals to read or modify. + :raises: InvalidDataError if a ReuseRepetition can't be filled + from the modals. + """ + if record.repetition is not None: + if isinstance(record.repetition, ReuseRepetition): + if modals.repetition is None: + raise InvalidDataError('Unfillable repetition') + else: + record.repetition = copy.copy(modals.repetition) + else: + modals.repetition = copy.copy(record.repetition) + + +def adjust_field(record: Record, r_field: str, modals: Modals, m_field: str): + """ + Merge record.r_field with modals.m_field + + :param record: Record to read or modify. + :param r_field: Attr of record to access. + :param modals: Modals to read or modify. + :param m_field: Attr of modals to access. + :raises: InvalidDataError if a both fields are None + """ + r = getattr(record, r_field) + if r is not None: + setattr(modals, m_field, r) + else: + m = getattr(modals, m_field) + if m is not None: + setattr(record, r_field, copy.copy(m)) + else: + raise InvalidDataError('Unfillable field: {}'.format(m_field)) + + +def adjust_coordinates(record: Record, modals: Modals, mx_field: str, my_field: str): + """ + Merge record.x and record.y with modals.mx_field and modals.my_field, + taking into account the value of modals.xy_relative. + + If modals.xy_relative is True and the record has non-None coordinates, + the modal values are added to the record's coordinates. If modals.xy_relative + is False, the coordinates are treated the same way as other fields. + + :param record: Record to read or modify. + :param modals: Modals to read or modify. + :param mx_field: Attr of modals corresponding to record.x + :param my_field: Attr of modals corresponding to record.y + :raises: InvalidDataError if a both fields are None + """ + if record.x is not None: + if modals.xy_relative: + record.x += getattr(modals, mx_field) + else: + setattr(modals, mx_field, record.x) + else: + record.x = getattr(modals, mx_field) + + if record.y is not None: + if modals.xy_relative: + record.y += getattr(modals, my_field) + else: + setattr(modals, my_field, record.y) + else: + record.y = getattr(modals, my_field) + + +# TODO: Clarify the docs on the dedup_* functions +def dedup_repetition(record: Record, modals: Modals): + """ + Deduplicate the record's repetition entry with the one in the modals. + Update the one in the modals if they are different. + + :param record: Record to read or modify. + :param modals: Modals to read or modify. + :raises: InvalidDataError if a ReuseRepetition can't be filled + from the modals. + """ + if record.repetition is None: + return + + if isinstance(record.repetition, ReuseRepetition): + if modals.repetition is None: + raise InvalidDataError('Unfillable repetition') + return + + if record.repetition == modals.repetition: + record.repetition = ReuseRepetition() + else: + modals.repetition = record.repetition + + +def dedup_field(record: Record, r_field: str, modals: Modals, m_field: str): + """ + Deduplicate record.r_field using modals.m_field + Update the modals.m_field if they are different. + + :param record: Record to read or modify. + :param r_field: Attr of record to access. + :param modals: Modals to read or modify. + :param m_field: Attr of modals to access. + :raises: InvalidDataError if a both fields are None + """ + r = getattr(record, r_field) + m = getattr(modals, m_field) + if r is not None: + if m is not None and m == r: + setattr(record, r_field, None) + else: + setattr(modals, m_field, r) + elif m is None: + raise InvalidDataError('Unfillable field') + + +def dedup_coordinates(record: Record, modals: Modals, mx_field: str, my_field: str): + """ + Deduplicate record.x and record.y using modals.mx_field and modals.my_field, + taking into account the value of modals.xy_relative. + + If modals.xy_relative is True and the record has non-None coordinates, + the modal values are subtracted from the record's coordinates. If modals.xy_relative + is False, the coordinates are treated the same way as other fields. + + :param record: Record to read or modify. + :param modals: Modals to read or modify. + :param mx_field: Attr of modals corresponding to record.x + :param my_field: Attr of modals corresponding to record.y + :raises: InvalidDataError if a both fields are None + """ + if record.x is not None: + mx = getattr(modals, mx_field) + if modals.xy_relative: + record.x -= mx + else: + if record.x == mx: + record.x = None + else: + setattr(modals, mx_field, record.x) + + if record.y is not None: + my = getattr(modals, my_field) + if modals.xy_relative: + record.y -= my + else: + if record.y == my: + record.y = None + else: + setattr(modals, my_field, record.y) + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..43edec2 --- /dev/null +++ b/setup.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 + +from setuptools import setup, find_packages + +setup(name='fatamorgana', + version='0.1', + description='OASIS layout format parser and writer' + author='Jan Petykiewicz', + author_email='anewusername@gmail.com', + url='https://mpxd.net/gogs/jan/fatamorgana', + keywords=[ + 'OASIS', + 'layout', + 'design', + 'CAD', + 'EDA', + 'oas', + 'electronics', + 'open', + 'artwork', + 'interchange', + 'standard', + 'mask', + 'pattern', + 'IC', + 'geometry', + 'geometric', + 'polygon', + 'gds', + ], + classifiers=[ + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Development Status :: 3 - Alpha', + 'Environment :: Other Environment', + 'Intended Audience :: Developers', + 'Intended Audience :: Information Technology', + 'Intended Audience :: Manufacturing', + 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: GNU Affero General Public License v3', + 'Operating System :: POSIX :: Linux', + 'Operating System :: Microsoft :: Windows', + 'Topic :: Scientific/Engineering', + 'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], + packages=find_packages(), + install_requires=[ + 'typing', + ], + extras_require={ + 'numpy': ['numpy'], + }, + ) +