diff --git a/.doc.template b/.doc.template new file mode 100644 index 0000000..c6dcad3 --- /dev/null +++ b/.doc.template @@ -0,0 +1,41 @@ +![logo](logo.png) +{{with .PDoc}}{{if $.IsMain}}> {{ base .ImportPath }} +{{comment_md .Doc}}{{else}}# {{ .Name }} +[![GoDoc](https://godoc.org/{{ .ImportPath }}?status.svg)](https://godoc.org/github.com/sbrow/{{ .Name }}) [![Build Status](https://travis-ci.org/sbrow/{{ .Name }}.svg?branch=master)](https://travis-ci.org/sbrow/{{ .Name }}) [![Coverage Status](https://coveralls.io/repos/github/sbrow/{{ .Name }}/badge.svg?branch=master)](https://coveralls.io/github/sbrow/{{ .Name }}?branch=master) [![Go Report Card](https://goreportcard.com/badge/{{ .ImportPath }})](https://goreportcard.com/report/{{ .ImportPath }}) + +`import "{{.ImportPath}}"` + +* [Overview](#pkg-overview) +* [Installation](pkg-installation){{if $.Dirs}} +* [Subdirectories](#pkg-subdirectories){{end}}{{ with .Notes}}{{ range $marker, $content := .}} +* [{{$marker}}](#pkg-note-{{$marker}}){{end}} +{{- end}} +* [Documentation](#pkg-doc) + +## Overview +{{comment_md .Doc}} +{{example_html $ ""}} + + +## Installation +```sh +$ go get -u {{ .ImportPath }} +``` + + +{{ with .Notes}} +{{ range $marker, $content := .}} +## {{$marker}} +{{ range $key, $value := .}} +`{{ $value.UID }}:` {{ $value.Body }}{{end}}{{end}}{{end}} +## Documentation +For full Documentation please visit https://godoc.org/{{.ImportPath}} +- - - +{{end}} + +Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..09be215 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ + +data/ +*.test +*.out diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000..2acf471 --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,2 @@ +Spencer Brower +psikoz \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d890164 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +![logo](logo.png) +# ps +[![GoDoc](https://godoc.org/github.com/sbrow/ps?status.svg)](https://godoc.org/github.com/sbrow/ps) [![Build Status](https://travis-ci.org/sbrow/ps.svg?branch=master)](https://travis-ci.org/sbrow/ps) [![Coverage Status](https://coveralls.io/repos/github/sbrow/ps/badge.svg?branch=master)](https://coveralls.io/github/sbrow/ps?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/sbrow/ps)](https://goreportcard.com/report/github.com/sbrow/ps) + +`import "github.com/sbrow/ps"` + +* [Overview](#pkg-overview) +* [Installation](pkg-installation) +* [Subdirectories](#pkg-subdirectories) +* [TODO](#pkg-note-TODO) +* [Documentation](#pkg-doc) + +## Overview +Package ps is a rudimentary API between Adobe Photoshop CS5 and Golang. +The interaction between the two is implemented using Javascript/VBScript. + +Use it to control Photoshop, edit documents, and perform batch operations. + +Currently only supports Photoshop CS5 Windows x86_64. + + + + + +## Installation +```sh +$ go get -u github.com/sbrow/ps +``` + + + + +## TODO + +`sbrow:` (2) Make TextLayer a subclass of ArtLayer. + +`sbrow:` Reduce cyclomatic complexity of ActiveDocument(). + +`sbrow:` refactor Close to Document.Close + +## Documentation +For full Documentation please visit https://godoc.org/github.com/sbrow/ps +- - - + + +Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md) diff --git a/Test.psd b/Test.psd new file mode 100644 index 0000000..129ebca Binary files /dev/null and b/Test.psd differ diff --git a/Variables.go b/Variables.go new file mode 100644 index 0000000..dc5df54 --- /dev/null +++ b/Variables.go @@ -0,0 +1,38 @@ +package ps + +// ModeEnum determines how aggressively the package will attempt to sync with Photoshop. +// Loading Photoshop files from scratch takes a long time, so the package saves +// the state of the document in a JSON file in the /data folder whenever you call +// Document.Dump(). ModeEnum tells the program how trustworthy that file is. +type ModeEnum int + +// Mode holds the current mode. +var Mode ModeEnum + +// Normal Mode only verifies layers as they are operated on. The first time a +// layer's properties would be checked, it first overwrites the data from the +// Dump with data pulled directly from Photoshop. This allows you to quickly +// load documents in their current form. +const Normal ModeEnum = 0 + +// Safe Mode always loads the document from scratch, ignoring any dumped data. +// (Very Slow). If a function panics due to outdated data, often times re-running +// the function in safe mode is enough to remediate it. +const Safe ModeEnum = 1 + +// Fast mode skips all verification. Use Fast mode only when certain that the +// .psd file hasn't changed since the last time Document.Dump() was called. +const Fast ModeEnum = 2 + +// SaveOption is an enum for options when closing a document. +type SaveOption int + +// SaveChanges Saves changes before closing documents. +const SaveChanges SaveOption = 1 + +// DoNotSaveChanges Closes documents without saving. +const DoNotSaveChanges SaveOption = 2 + +// PromptToSaveChanges prompts the user whether the file +// should be saved before closing. +const PromptToSaveChanges SaveOption = 3 diff --git a/artlayer.go b/artlayer.go new file mode 100644 index 0000000..d088482 --- /dev/null +++ b/artlayer.go @@ -0,0 +1,286 @@ +package ps + +import ( + "encoding/json" + "fmt" + "log" + + "github.com/sbrow/ps/runner" +) + +// ArtLayer reflects some values from an Art Layer in a Photoshop document. +// +// TODO(sbrow): (2) Make TextLayer a subclass of ArtLayer. +type ArtLayer struct { + name string // The layer's name. + bounds [2][2]int // The corners of the layer's bounding box. + parent Group // The LayerSet/Document this layer is in. + visible bool // Whether or not the layer is visible. + current bool // Whether we've checked this layer since we loaded from disk. + Color Color // The layer's color overlay effect (if any). + Stroke *Stroke // The layer's stroke effect (if any). + *TextItem // The layer's text, if it's a text layer. +} + +// Bounds returns the coordinates of the corners of the ArtLayer's bounding box. +func (a *ArtLayer) Bounds() [2][2]int { + return a.bounds +} + +// ArtLayerJSON is a bridge between the ArtLayer struct and +// the encoding/json package, allowing ArtLayer's unexported fields +// to ber written to and read from by the json package. +type ArtLayerJSON struct { + Name string + Bounds [2][2]int + Color [3]int + Stroke [3]int + StrokeAmt float32 + Visible bool + TextItem *TextItem +} + +// MarshalJSON implements the json.Marshaler interface, allowing the ArtLayer to be +// saved to disk in JSON format. +func (a *ArtLayer) MarshalJSON() ([]byte, error) { + return json.Marshal(&ArtLayerJSON{ + Name: a.name, + Bounds: a.bounds, + Visible: a.visible, + Color: a.Color.RGB(), + Stroke: a.Stroke.RGB(), + StrokeAmt: a.Stroke.Size, + TextItem: a.TextItem, + }) +} + +// UnmarshalJSON loads json data into the object. +func (a *ArtLayer) UnmarshalJSON(b []byte) error { + tmp := &ArtLayerJSON{} + if err := json.Unmarshal(b, &tmp); err != nil { + return err + } + a.name = tmp.Name + a.bounds = tmp.Bounds + a.Color = RGB{tmp.Color[0], tmp.Color[1], tmp.Color[2]} + a.Stroke = &Stroke{tmp.StrokeAmt, RGB{tmp.Stroke[0], tmp.Stroke[1], tmp.Stroke[2]}} + a.visible = tmp.Visible + a.current = false + a.TextItem = tmp.TextItem + if a.TextItem != nil { + a.TextItem.parent = a + } + return nil +} + +// Name returns the layer's name. +func (a *ArtLayer) Name() string { + return a.name +} + +// Parent returns the Document or LayerSet this layer is contained in. +func (a *ArtLayer) Parent() Group { + return a.parent +} + +// X1 returns the layer's leftmost x value. +func (a *ArtLayer) X1() int { + return a.bounds[0][0] +} + +// X2 returns the layer's rightmost x value. +func (a *ArtLayer) X2() int { + return a.bounds[1][0] +} + +// Y1 returns the layer's topmost y value. +func (a *ArtLayer) Y1() int { + return a.bounds[0][1] +} + +// Y2 returns the layer's bottommost y value. +func (a *ArtLayer) Y2() int { + return a.bounds[1][1] +} + +// SetParent sets Group c to be the group that holds this layer. +func (a *ArtLayer) SetParent(c Group) { + a.parent = c +} + +// SetActive makes this layer active in Photoshop. +// Layers need to be active to perform certain operations +func (a *ArtLayer) SetActive() ([]byte, error) { + js := fmt.Sprintf("app.activeDocument.activeLayer=%s", JSLayer(a.Path())) + return DoJS("compilejs.jsx", js) +} + +// SetColor creates a color overlay for the layer +func (a *ArtLayer) SetColor(c Color) { + if a.Color.RGB() == c.RGB() { + if Mode == 2 || (Mode == 0 && a.current) { + // log.Println("Skipping color: already set.") + return + } + } + if a.Stroke.Size != 0 { + a.SetStroke(*a.Stroke, c) + return + } + a.Color = c + cols := a.Color.RGB() + log.Printf(`Setting layer "%s" to color %v`, a.name, cols) + r := cols[0] + g := cols[1] + b := cols[2] + byt, err := a.SetActive() + if len(byt) != 0 { + log.Println(string(byt), "err") + } + if err != nil { + log.Println(a.Path()) + log.Panic(err) + } + byt, err = runner.Run("colorLayer", fmt.Sprint(r), fmt.Sprint(g), fmt.Sprint(b)) + if len(byt) != 0 { + log.Println(string(byt), "err") + } + if err != nil { + log.Panic(err) + } +} + +// SetStroke edits the "aura" around the layer. If a nil stroke is given, +// The current stroke is removed. If a non-nil stroke is given and the +// current stroke is nil, a stroke is added. +func (a *ArtLayer) SetStroke(stk Stroke, fill Color) { + if stk.Size == 0 { + a.Stroke = &stk + a.SetColor(fill) + return + } + if fill == nil { + fill = a.Color + } + if stk.Size == a.Stroke.Size && stk.Color.RGB() == a.Stroke.Color.RGB() { + if a.Color.RGB() == fill.RGB() { + if Mode == 2 || (Mode == 0 && a.current) { + // log.Println("Skipping stroke: already set.") + return + } + } + } + byt, err := a.SetActive() + if len(byt) != 0 { + log.Println(string(byt)) + } + if err != nil { + log.Panic(err) + } + a.Stroke = &stk + a.Color = fill + stkCol := stk.Color.RGB() + col := fill.RGB() + log.Printf("Setting layer %s stroke to %.2fpt %v and color to %v\n", a.name, a.Stroke.Size, + a.Stroke.Color.RGB(), a.Color.RGB()) + byt, err = runner.Run("colorStroke", fmt.Sprint(col[0]), fmt.Sprint(col[1]), fmt.Sprint(col[2]), + fmt.Sprintf("%.2f", stk.Size), fmt.Sprint(stkCol[0]), fmt.Sprint(stkCol[1]), fmt.Sprint(stkCol[2])) + if len(byt) != 0 { + log.Println(string(byt)) + } + if err != nil { + log.Panic(err) + } +} + +// Path returns the Path to this layer, through all of its parents. +func (a *ArtLayer) Path() string { + return fmt.Sprintf("%s%s", a.parent.Path(), a.name) +} + +// SetVisible makes the layer visible. +func (a *ArtLayer) SetVisible(b bool) error { + if a.visible == b { + return nil + } + a.visible = b + switch b { + case true: + log.Printf("Showing %s", a.name) + case false: + log.Printf("Hiding %s", a.name) + } + js := fmt.Sprintf("%s.visible=%v;", JSLayer(a.Path()), b) + if byt, err := DoJS("compilejs.jsx", js); err != nil { + log.Println(string(byt)) + return err + } + return nil +} + +// Visible returns whether or not the layer is currently hidden. +func (a *ArtLayer) Visible() bool { + return a.visible +} + +// SetPos snaps the given layer boundary to the given point. +// Valid options for bound are: "TL", "TR", "BL", "BR" +func (a *ArtLayer) SetPos(x, y int, bound string) { + if !a.visible || (x == 0 && y == 0) { + return + } + var lyrX, lyrY int + switch bound[:1] { + case "B": + lyrY = a.Y2() + case "T": + fallthrough + default: + lyrY = a.Y1() + } + switch bound[1:] { + case "R": + lyrX = a.X2() + case "L": + fallthrough + default: + lyrX = a.X1() + } + byt, err := DoJS("moveLayer.jsx", JSLayer(a.Path()), + fmt.Sprint(x-lyrX), fmt.Sprint(y-lyrY), + ) + if err != nil { + fmt.Printf("%+v %+v\n", a.Parent(), a.Path()) + panic(err) + } + var lyr ArtLayer + err = json.Unmarshal(byt, &lyr) + if err != nil { + log.Panic(err) + } + a.bounds = lyr.bounds +} + +// Refresh syncs the layer with Photoshop. +func (a *ArtLayer) Refresh() error { + var tmp *ArtLayer + data, err := DoJS("getLayer.jsx", JSLayer(a.Path())) + if err != nil && len(err.Error()) > 0 { + return err + } + if err = json.Unmarshal(data, &tmp); err != nil { + fmt.Println(string(data)) + return err + } + tmp.SetParent(a.Parent()) + a.name = tmp.name + a.bounds = tmp.bounds + a.TextItem = tmp.TextItem + if a.TextItem != nil { + a.TextItem.parent = a + } + a.parent = tmp.Parent() + a.visible = tmp.visible + a.current = true + return nil +} diff --git a/colors.go b/colors.go new file mode 100644 index 0000000..80623e7 --- /dev/null +++ b/colors.go @@ -0,0 +1,77 @@ +package ps + +import "encoding/hex" + +// Some basic colors. +var ( + ColorBlack Color = RGB{0, 0, 0} + ColorGray Color = RGB{128, 128, 128} + ColorWhite Color = RGB{255, 255, 255} +) + +// Color is an interface for color objects, allowing colors to be +// used in various formats. +// +// RGB is the default format for everything. +type Color interface { + RGB() [3]int // The color in RGB format. + Hex() []uint8 // The color in hexadecimal format. +} + +// Compare returns the brighter of a and b. +func Compare(a, b Color) Color { + A := a.RGB() + B := b.RGB() + Aavg := (A[0] + A[1] + A[2]) / 3 + Bavg := (B[0] + B[1] + B[2]) / 3 + if Aavg > Bavg { + return a + } + return b +} + +// RGB is a color format. It implements the Color interface. +type RGB struct { + Red int + Green int + Blue int +} + +// RGB returns the color in RGB format. +func (r RGB) RGB() [3]int { + return [3]int{r.Red, r.Green, r.Blue} +} + +// Hex returns the color coverted to hexidecimal format. +func (r RGB) Hex() []uint8 { + src := []uint8{uint8(r.Red), uint8(r.Green), uint8(r.Blue)} + hex := make([]byte, hex.EncodedLen(len(src))) + return hex +} + +// Hex is a color in hexadecimal format. +// It satisfies the Color interface. +type Hex []uint8 + +// RGB returns the Hex value converted to RGB +func (h Hex) RGB() [3]int { + src := []byte(h) + dst := make([]byte, hex.DecodedLen(len(src))) + _, err := hex.Decode(dst, src) + if err != nil { + panic(err) + } + return [3]int{int(dst[0]), int(dst[1]), int(dst[2])} +} + +// Hex returns the hex value of the number, +// to satisfy the Color interface. +func (h Hex) Hex() []uint8 { + return h +} + +// Stroke represents a layer stroke effect. +type Stroke struct { + Size float32 + Color +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..7296bcd --- /dev/null +++ b/doc.go @@ -0,0 +1,7 @@ +// Package ps is a rudimentary API between Adobe Photoshop CS5 and Golang. +// The interaction between the two is implemented using Javascript/VBScript. +// +// Use it to control Photoshop, edit documents, and perform batch operations. +// +// Currently only supports Photoshop CS5 Windows x86_64. +package ps diff --git a/document.go b/document.go new file mode 100644 index 0000000..0c8503f --- /dev/null +++ b/document.go @@ -0,0 +1,255 @@ +package ps + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "os" + "os/user" + "path/filepath" + "strings" +) + +// Document represents a Photoshop document (PSD file). +type Document struct { + name string + fullName string + height int + width int + artLayers []*ArtLayer + layerSets []*LayerSet +} + +// DocumentJSON is an exported version of Document that +// allows Documents to be saved to and loaded from JSON. +type DocumentJSON struct { + Name string + FullName string + Height int + Width int + ArtLayers []*ArtLayer + LayerSets []*LayerSet +} + +// MarshalJSON returns the Document in JSON format. +func (d *Document) MarshalJSON() ([]byte, error) { + return json.Marshal(&DocumentJSON{Name: d.name, FullName: d.fullName, Height: d.height, + Width: d.width, ArtLayers: d.artLayers, LayerSets: d.layerSets}) +} + +// UnmarshalJSON loads JSON data into this Document. +func (d *Document) UnmarshalJSON(b []byte) error { + tmp := &DocumentJSON{} + if err := json.Unmarshal(b, &tmp); err != nil { + return err + } + d.name = tmp.Name + d.fullName = tmp.FullName + d.height = tmp.Height + d.width = tmp.Width + d.artLayers = tmp.ArtLayers + for _, lyr := range d.artLayers { + lyr.SetParent(d) + } + d.layerSets = tmp.LayerSets + for _, set := range d.layerSets { + set.SetParent(d) + } + return nil +} + +// Name returns the document's title. +// This fulfills the Group interface. +func (d *Document) Name() string { + return d.name +} + +// FullName returns the absolute path to the current document file. +func (d *Document) FullName() string { + return d.fullName +} + +// Parent returns the Group that contains d. +func (d *Document) Parent() Group { + return nil +} + +// Height returns the height of the document, in pixels. +func (d *Document) Height() int { + return d.height +} + +// ArtLayer returns the first top level ArtLayer matching +// the given name. +func (d *Document) ArtLayer(name string) *ArtLayer { + for _, lyr := range d.artLayers { + if lyr.name == name { + if Mode == 0 && !lyr.current { + err := lyr.Refresh() + if err != nil { + log.Panic(err) + } + } + return lyr + } + } + return nil +} + +// ArtLayers returns this document's ArtLayers, if any. +func (d *Document) ArtLayers() []*ArtLayer { + return d.artLayers +} + +// LayerSets returns all the document's top level LayerSets. +func (d *Document) LayerSets() []*LayerSet { + return d.layerSets +} + +// LayerSet returns the first top level LayerSet matching +// the given name. +func (d *Document) LayerSet(name string) *LayerSet { + for _, set := range d.layerSets { + if set.name == name { + if Mode != Fast && !set.current { + if err := set.Refresh(); err != nil { + log.Panic(err) + } + } + return set + } + } + return nil +} + +// ActiveDocument returns document currently focused in Photoshop. +// +// TODO(sbrow): Reduce cyclomatic complexity of ActiveDocument(). +func ActiveDocument() (*Document, error) { + log.Println("Loading ActiveDoucment") + d := &Document{} + + byt, err := DoJS("activeDocName.jsx") + if err != nil { + return nil, err + } + d.name = strings.TrimRight(string(byt), "\r\n") + if Mode != Safe { + err = d.Restore(d.DumpFile()) + switch { + case os.IsNotExist(err): + log.Println("Previous version not found.") + case err == nil: + return d, err + default: + return nil, err + + } + } + log.Println("Loading manually (This could take awhile)") + byt, err = DoJS("getActiveDoc.jsx") + if err != nil { + return nil, err + } + if err = json.Unmarshal(byt, &d); err != nil { + d.Dump() + return nil, err + } + for _, lyr := range d.artLayers { + lyr.SetParent(d) + } + for i, set := range d.layerSets { + var s *LayerSet + if s, err = NewLayerSet(set.Path()+"/", d); err != nil { + return nil, err + } + d.layerSets[i] = s + s.SetParent(d) + } + d.Dump() + return d, err +} + +// Restore loads document data from a JSON file. +func (d *Document) Restore(path string) error { + if path == "" { + path = d.DumpFile() + } + byt, err := ioutil.ReadFile(path) + if err == nil { + log.Println("Previous version found, loading") + err = json.Unmarshal(byt, &d) + } + return err +} + +// SetParent does nothing, as the document is a top-level object +// and therefore can't have a parent group. +// The function is needed to implement the group interface. +func (d *Document) SetParent(g Group) {} + +// Path returns the root path ("") for all the layers. +func (d *Document) Path() string { + return "" +} + +// DumpFile returns the path to the json file where +// this document's data gets dumped. See Document.Dump +func (d *Document) DumpFile() string { + usr, err := user.Current() + if err != nil { + log.Println(err) + } + path := filepath.Join(strings.Replace(d.fullName, "~", usr.HomeDir, 1)) + return strings.Replace(path, ".psd", ".json", 1) +} + +// Dump saves the document to disk in JSON format. +func (d *Document) Dump() { + log.Println("Dumping to disk") + log.Println(d.DumpFile()) + defer d.Save() + f, err := os.Create(d.DumpFile()) + if err != nil { + log.Fatal(err) + } + defer func() { + if err = f.Close(); err != nil { + log.Println(err) + } + }() + byt, err := json.MarshalIndent(d, "", "\t") + if err != nil { + log.Fatal(err) + } + if _, err = f.Write(byt); err != nil { + log.Println(err) + } +} + +// MustExist returns a Layer from the set with the given name, and +// panics if it doesn't exist. +// +// If there is a LayerSet and an ArtLayer with the same name, +// it will return the LayerSet. +func (d *Document) MustExist(name string) Layer { + set := d.LayerSet(name) + if set == nil { + lyr := d.ArtLayer(name) + if lyr == nil { + log.Panicf("no Layer found at \"%s%s\"", d.Path(), name) + } + return lyr + } + return set +} + +// Save saves the Document in place. +func (d *Document) Save() error { + js := fmt.Sprintf("var d=app.open(File('%s'));\nd.save();", d.FullName()) + if _, err := DoJS("compilejs", js); err != nil { + return err + } + return nil +} diff --git a/document_test.go b/document_test.go new file mode 100644 index 0000000..af1a793 --- /dev/null +++ b/document_test.go @@ -0,0 +1,106 @@ +package ps + +import ( + "log" + "os" + "path/filepath" + "reflect" + "runtime" + "testing" +) + +// wd is the working directory +var wd string + +func init() { + _, file, _, ok := runtime.Caller(0) + if !ok { + log.Fatal("runtime.Caller(0) returned !ok") + } + wd = filepath.Dir(file) + + f, err := os.Create("test.log") + if err != nil { + log.Fatal(err) + } + log.SetOutput(f) +} + +func TestDocument_Dump(t *testing.T) { + // Must be true for test to be valid. + Mode = Normal + + tests := []struct { + name string + }{ + {"Test"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + // Get rid of old Dump. + os.Remove(filepath.Join(wd, tt.name+".json")) + + // Generate a fresh Doc (loaded slowly). + want, err := Open(filepath.Join(wd, tt.name+".psd")) + if err != nil { + t.Fatal(err) + } + + // Dump the contents. + want.Dump() + // Grab a new version of the doc (loaded from json). + got, err := Open(filepath.Join(wd, tt.name+".psd")) + if err != nil { + t.Fatal(err) + } + got.layerSets[0].current = true + if !reflect.DeepEqual(got, want) { + t.Errorf("wanted: %+v\ngot: %+v", want, got) + } + }) + } +} + +func TestDocument_Save(t *testing.T) { + file := filepath.Join(wd, "Test.psd") + d, err := Open(file) + if err != nil { + t.Fatal(err) + } + + layerName := "Group 1" + lyr := d.LayerSet(layerName) + if lyr == nil { + t.Fatalf("LayerSet '%s' was not found", layerName) + } + + // Change a layer name. + _, err = DoJS(filepath.Join("SetName"), JSLayer(lyr.Path()), "Group 2") + if err != nil { + t.Error(err) + } + + defer func() { + if err := DoAction("DK", "Undo"); err != nil { + t.Error(err) + } + err := d.Save() + if err != nil { + t.Fatal(err) + } + }() + + err = d.Save() + if err != nil { + t.Fatal(err) + } + + d2, err := Open(file) + if err != nil { + t.Fatal(err) + } + if reflect.DeepEqual(d, d2) { + t.Errorf("wanted: %+v\ngot: %+v", d, d2) + } +} diff --git a/examples_test.go b/examples_test.go new file mode 100644 index 0000000..94b69d8 --- /dev/null +++ b/examples_test.go @@ -0,0 +1,10 @@ +package ps + +import "fmt" + +func ExampleJSLayer() { + // The path of a layer inside a top level group. + path := "Group 1/Layer 1" + fmt.Println(JSLayer(path)) + // Output: app.activeDocument.layerSets.getByName('Group 1').artLayers.getByName('Layer 1') +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..de251c1 --- /dev/null +++ b/go.mod @@ -0,0 +1 @@ +module github.com/sbrow/ps diff --git a/interfaces.go b/interfaces.go new file mode 100644 index 0000000..e454760 --- /dev/null +++ b/interfaces.go @@ -0,0 +1,31 @@ +package ps + +// Group represents a Document or LayerSet. +type Group interface { + Name() string + Parent() Group + SetParent(Group) + Path() string + ArtLayer(name string) *ArtLayer + LayerSet(name string) *LayerSet + ArtLayers() []*ArtLayer + LayerSets() []*LayerSet + MustExist(name string) Layer + MarshalJSON() ([]byte, error) + UnmarshalJSON(b []byte) error +} + +// Layer represents an ArtLayer or LayerSet Object. +type Layer interface { + Bounds() [2][2]int + MarshalJSON() ([]byte, error) + Name() string + Parent() Group + Path() string + Refresh() error + SetParent(g Group) + SetPos(x, y int, bound string) + SetVisible(b bool) error + UnmarshalJSON(b []byte) error + Visible() bool +} diff --git a/layerset.go b/layerset.go new file mode 100644 index 0000000..d04ddb1 --- /dev/null +++ b/layerset.go @@ -0,0 +1,298 @@ +package ps + +import ( + "encoding/json" + "fmt" + "log" + "strings" +) + +// LayerSet holds a group of Layer objects and a group of LayerSet objects. +type LayerSet struct { + name string + bounds [2][2]int + parent Group + current bool // Whether we've checked this layer since we loaded from disk. + visible bool + artLayers []*ArtLayer + layerSets []*LayerSet +} + +// LayerSetJSON is an exported version of LayerSet, that allows LayerSets to be +// saved to and loaded from JSON. +type LayerSetJSON struct { + Name string + Bounds [2][2]int + Visible bool + ArtLayers []*ArtLayer + LayerSets []*LayerSet +} + +// MarshalJSON returns the LayerSet in JSON form. +func (l *LayerSet) MarshalJSON() ([]byte, error) { + return json.Marshal(&LayerSetJSON{ + Name: l.name, + Bounds: l.bounds, + Visible: l.visible, + ArtLayers: l.artLayers, + LayerSets: l.layerSets, + }) +} + +// UnmarshalJSON loads the json data into this LayerSet. +func (l *LayerSet) UnmarshalJSON(b []byte) error { + tmp := &LayerSetJSON{} + if err := json.Unmarshal(b, &tmp); err != nil { + return err + } + l.name = tmp.Name + l.bounds = tmp.Bounds + l.visible = tmp.Visible + l.artLayers = tmp.ArtLayers + for _, lyr := range l.artLayers { + lyr.SetParent(l) + } + l.layerSets = tmp.LayerSets + for _, set := range l.layerSets { + set.SetParent(l) + } + l.current = false + return nil +} + +// Name returns the name of the LayerSet +func (l LayerSet) Name() string { + return l.name +} + +// ArtLayers returns the LayerSet's ArtLayers. +func (l *LayerSet) ArtLayers() []*ArtLayer { + if Mode != Fast { + for _, lyr := range l.artLayers { + if !lyr.current { + if err := lyr.Refresh(); err != nil { + log.Println(err) + } + lyr.current = true + } + } + } + return l.artLayers +} + +// ArtLayer returns the first top level ArtLayer matching +// the given name. +func (l *LayerSet) ArtLayer(name string) *ArtLayer { + for _, lyr := range l.artLayers { + if lyr.name == name { + if Mode == 0 && !lyr.current { + err := lyr.Refresh() + if err != nil { + if err = l.Refresh(); err != nil { + log.Panic(err) + } + if err := lyr.Refresh(); err != nil { + log.Panic(err) + } + } + } + return lyr + } + } + return nil +} + +// LayerSets returns the LayerSets contained within +// this set. +func (l *LayerSet) LayerSets() []*LayerSet { + return l.layerSets +} + +// LayerSet returns the first top level LayerSet matching +// the given name. +func (l *LayerSet) LayerSet(name string) *LayerSet { + for _, set := range l.layerSets { + if set.name == name { + return set + } + } + return nil +} + +// MustExist returns a Layer from the set with the given name, and +// panics if it doesn't exist. +// +// If there is a LayerSet and an ArtLayer with the same name, +// it will return the LayerSet. +func (l *LayerSet) MustExist(name string) Layer { + set := l.LayerSet(name) + if set == nil { + lyr := l.ArtLayer(name) + if lyr == nil { + log.Panicf("no Layer found at \"%s%s\"", l.Path(), name) + } + return lyr + } + return set +} + +// Bounds returns the furthest corners of the LayerSet. +func (l LayerSet) Bounds() [2][2]int { + return l.bounds +} + +// SetParent puts this LayerSet into the given group. +func (l *LayerSet) SetParent(g Group) { + l.parent = g +} + +// Parent returns this layerSet's parent. +func (l *LayerSet) Parent() Group { + return l.parent +} + +// Path returns the layer path to this Set. +func (l *LayerSet) Path() string { + if l.parent == nil { + return l.name + } + return fmt.Sprintf("%s%s/", l.parent.Path(), l.name) +} + +// NewLayerSet grabs the LayerSet with the given path and returns it. +func NewLayerSet(path string, g Group) (*LayerSet, error) { + path = strings.Replace(path, "//", "/", -1) + byt, err := DoJS("getLayerSet.jsx", JSLayer(path), JSLayerMerge(path)) + if err != nil { + return nil, err + } + var out *LayerSet + err = json.Unmarshal(byt, &out) + if err != nil { + log.Println(JSLayer(path)) + log.Println(string(byt)) + return nil, err + } + out.SetParent(g) + log.Printf("Loading ActiveDocument/%s\n", out.Path()) + for _, lyr := range out.artLayers { + lyr.SetParent(out) + } + for i, set := range out.layerSets { + var s *LayerSet + s, err = NewLayerSet(fmt.Sprintf("%s%s/", path, set.Name()), out) + if err != nil { + log.Fatal(err) + } + out.layerSets[i] = s + s.SetParent(out) + } + out.current = true + return out, err +} + +// Visible returns whether or not the LayerSet is currently visible. +func (l LayerSet) Visible() bool { + return l.visible +} + +// SetVisible makes the LayerSet visible. +func (l *LayerSet) SetVisible(b bool) error { + if l.visible == b { + return nil + } + js := fmt.Sprintf("%s.visible=%v;", JSLayer(l.Path()), b) + if _, err := DoJS("compilejs.jsx", js); err != nil { + return err + } + l.visible = b + return nil +} + +// SetPos snaps the given layerset boundry to the given point. +// Valid options for bound are: "TL", "TR", "BL", "BR" +func (l *LayerSet) SetPos(x, y int, bound string) { + if !l.visible || (x == 0 && y == 0) { + return + } + path := JSLayer(l.Path()) + mrgPath := JSLayerMerge(l.Path()) + byt, err := DoJS("LayerSetBounds.jsx", path, mrgPath) + if err != nil { + log.Println(string(byt)) + log.Panic(err) + } + var bnds *[2][2]int + err = json.Unmarshal(byt, &bnds) + if err != nil { + fmt.Println(string(byt)) + log.Panic(err) + } + l.bounds = *bnds + var lyrX, lyrY int + switch bound[:1] { + case "B": + lyrY = l.bounds[1][1] + case "T": + fallthrough + default: + lyrY = l.bounds[0][1] + } + switch bound[1:] { + case "R": + lyrX = l.bounds[1][0] + case "L": + fallthrough + default: + lyrX = l.bounds[0][0] + } + byt, err = DoJS("moveLayer.jsx", JSLayer(l.Path()), fmt.Sprint(x-lyrX), + fmt.Sprint(y-lyrY), JSLayerMerge(l.Path())) + if err != nil { + fmt.Println("byte:", string(byt)) + panic(err) + } + var lyr LayerSet + if err = json.Unmarshal(byt, &lyr); err != nil { + fmt.Println("byte:", string(byt)) + log.Panic(err) + } + l.bounds = lyr.bounds +} + +// Refresh syncs the LayerSet with Photoshop. +func (l *LayerSet) Refresh() error { + var tmp *LayerSet + byt, err := DoJS("getLayerSet.jsx", JSLayer(l.Path())) + if err != nil { + if err = DoAction("Undo", "DK"); err != nil { + return err + } + if byt, err = DoJS("getLayerSet.jsx", JSLayer(l.Path())); err != nil { + return err + } + } + err = json.Unmarshal(byt, &tmp) + if err != nil { + log.Println("Error in LayerSet.Refresh() \"", string(byt), "\"", "for", l.Path()) + return err + } + tmp.SetParent(l.Parent()) + for _, lyr := range l.artLayers { + err = lyr.Refresh() + if err != nil { + l.artLayers = tmp.artLayers + break + } + } + for _, set := range l.layerSets { + if err = set.Refresh(); err != nil { + return err + } + } + l.name = tmp.name + l.bounds = tmp.bounds + l.visible = tmp.visible + l.current = true + return nil +} diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..99fe105 Binary files /dev/null and b/logo.png differ diff --git a/ps.go b/ps.go new file mode 100644 index 0000000..93a8afd --- /dev/null +++ b/ps.go @@ -0,0 +1,162 @@ +//go:generate sh -c "godoc2md -template ./.doc.template github.com/sbrow/ps > README.md" + +package ps + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "regexp" + "runtime" + "strings" + + "github.com/sbrow/ps/runner" +) + +// The full path to this directory. +var pkgPath string + +func init() { + _, file, _, _ := runtime.Caller(0) + pkgPath = filepath.Dir(file) +} + +// ApplyDataset fills out a template file with information +// from a given dataset (csv) file. It's important to note that running this +// function will change data in the Photoshop document, but will not update +// data in the Go Document struct- you will have to implement syncing +// them yourself. +func ApplyDataset(name string) error { + _, err := DoJS("applyDataset.jsx", name) + return err +} + +// Close closes the active document in Photoshop, using the given save option. +// TODO(sbrow): refactor Close to Document.Close +func Close(save SaveOption) error { + _, err := runner.Run("close", fmt.Sprint(save)) + return err +} + +// DoAction runs the Photoshop Action with name 'action' from ActionSet 'set'. +func DoAction(set, action string) error { + _, err := runner.Run("action", set, action) + return err +} + +// DoJS runs a Photoshop Javascript script file (.jsx) from the specified location. +// The script can't directly return output, so instead it writes output to +// a temporary file ($TEMP/js_out.txt), whose contents is then read and returned. +func DoJS(path string, args ...string) ([]byte, error) { + // var err error + // Temp file for js to output to. + outpath, err := ioutil.TempFile(os.Getenv("TEMP"), "") + defer func() { + if err = outpath.Close(); err != nil { + log.Println(err) + } + if err = os.Remove(outpath.Name()); err != nil { + log.Println(err) + } + }() + + if err != nil { + return nil, err + } + if !strings.HasSuffix(path, ".jsx") { + path += ".jsx" + } + + args = append([]string{outpath.Name()}, args...) + + // If passed a script by name, assume it's in the default folder. + scripts := filepath.Join(pkgPath, "runner", "scripts") + b, err := filepath.Match(scripts, path) + if !b || err != nil { + path = filepath.Join(scripts, path) + } + + args = append([]string{path}, args...) + cmd, err := runner.Run("dojs", args...) + if err == nil { + var data []byte + data, err = ioutil.ReadFile(outpath.Name()) + if err == nil { + cmd = append(cmd, data...) + } + } + return cmd, err +} + +// Init opens Photoshop if it is not open already. +// +// Init should be called before all other +func Init() error { + _, err := runner.Run("start") + return err +} + +// JSLayer "compiles" Javascript code to get an ArtLayer with the given path. +// The output always ends with a semicolon, so if you want to access a specific +// property of the layer, you'll have to trim the output before concatenating. +func JSLayer(path string) string { + pth := strings.Split(path, "/") + js := "app.activeDocument" + last := len(pth) - 1 + if last > 0 { + for i := 0; i < last; i++ { + js += fmt.Sprintf(".layerSets.getByName('%s')", pth[i]) + } + } + if pth[last] != "" { + js += fmt.Sprintf(".artLayers.getByName('%s')", pth[last]) + } + return js +} + +// JSLayerMerge gets the Javascript code to get the Layer or LayerSet with this path +// and returns the result if you were to merge the bottom-most LayerSet. +// +// If the bottom-most Object in the path is not a LayerSet, it will returns the same +// results as JSLayer. +func JSLayerMerge(path string) string { + reg := regexp.MustCompile(`layerSets(\.getByName\('[^']*'\)($|[^.]))`) + return reg.ReplaceAllString(JSLayer(path), "artLayers$1") +} + +// Open opens a Photoshop document with the specified path. +// If Photoshop is not currently running, it is started before +// opening the document. +func Open(path string) (*Document, error) { + if _, err := runner.Run("open", path); err != nil { + return nil, err + } + return ActiveDocument() +} + +// Quit exits Photoshop, closing all open documents using the given save option. +func Quit(save SaveOption) error { + _, err := runner.Run("quit", fmt.Sprint(save)) + return err +} + +// SaveAs saves the Photoshop document to the given location. +func SaveAs(path string) error { + _, err := runner.Run("save", path) + return err +} + +// Wait prints a message to the console and halts operation until the user +// signals that they are ready to continue (by pushing enter). +// +// Useful for when you need to do something by hand in the middle of an +// otherwise automated process, (e.g. importing a dataset). +func Wait(msg string) { + var input string + + fmt.Print(msg) + fmt.Scanln(&input) + fmt.Println() +} diff --git a/ps_test.go b/ps_test.go new file mode 100644 index 0000000..d81020e --- /dev/null +++ b/ps_test.go @@ -0,0 +1,349 @@ +// TODO(sbrow): Update package tests. +package ps + +import ( + "fmt" + "testing" + + "github.com/sbrow/ps/runner" +) + +/* +var testDoc string + +func init() { + Mode = Normal + log.Printf("Running in mode %v\n", Mode) + testDoc = filepath.Join(pkgpath, "test.psd") +} + +func TestPkgPath(t *testing.T) { + want := filepath.Join(os.Getenv("GOPATH"), "src", "github.com", "sbrow", "ps") + got := filepath.Join(pkgpath) + if got != want { + t.Errorf("wanted: %s\ngot: %s", want, got) + } +} + +func TestInit(t *testing.T) { + if testing.Short() { + t.Skip("Skipping \"TestStart\"") + } + if err := Quit(2); err != nil { + log.Println("error:", err) + } + if err := Init(); err != nil { + t.Error(err) + } +} + +func TestOpen(t *testing.T) { + if testing.Short() { + t.Skip("Skipping \"TestOpen\"") + } + if err := Init(); err != nil { + t.Fatal(err) + } + if err := Open(testDoc); err != nil { + log.Println(testDoc) + t.Fatal(err) + } +} + +func TestClose(t *testing.T) { + if testing.Short() { + t.Skip("Skipping \"TestClose\"") + } + var err error + if err = Open(testDoc); err != nil { + log.Println(testDoc) + t.Fatal(err) + } + if err = Close(DoNotSaveChanges); err != nil { + t.Fatal(err) + } +} + +func TestQuit(t *testing.T) { + if testing.Short() { + t.Skip("Skipping \"TestQuit\"") + } + Init() + err := Quit(DoNotSaveChanges) + if err != nil { + t.Fatal(err) + } +} + +func TestDoJs(t *testing.T) { + var err error + if err = Open(testDoc); err != nil { + t.Fatal(err) + } + want := "F:\\\\TEMP\\\\[0-9]*\r\narg\r\nargs\r\n" + script := "test.jsx" + var got []byte + if got, err = DoJS(script, "arg", "args"); err != nil { + t.Fatal(err) + } + if b, err := regexp.Match(want, got); err != nil || !b { + fail := fmt.Sprintf("wanted: %s\ngot: %s err: %s\n", want, got, err) + t.Error(fail) + } +} + +func TestRun(t *testing.T) { + out := []byte("hello,\r\nworld!\r\n") + msg, err := runner.Run("test", "hello,", "world!") + if err != nil { + t.Fatal(err) + } + if string(msg) != string(out) { + fail := fmt.Sprintf("TestRun failed.\ngot:\n\"%s\"\nwant:\n\"%s\"\n", msg, out) + t.Fatal(fail) + } +} + +func TestWait(t *testing.T) { + Wait("Waiting...") +} + +func TestDoAction_Crop(t *testing.T) { + if testing.Short() { + t.Skip("Skipping \"TestDoAction_Crop\"") + } + var err error + if err = Open(testDoc); err != nil { + t.Fatal(err) + } + if err = DoAction("DK", "Crop"); err != nil { + t.Error(err) + } +} + +func TestDoAction_Undo(t *testing.T) { + if testing.Short() { + t.Skip("Skipping \"TestDoAction_Undo\"") + } + if err := DoAction("DK", "Undo"); err != nil { + t.Fatal(err) + } +} + +func TestSaveAs(t *testing.T) { + if err := SaveAs("F:\\TEMP\\test.png"); err != nil { + t.Fatal(err) + } + os.Remove("F:\\TEMP\\test.png") +} + +func TestLayerSet(t *testing.T) { + if _, err := NewLayerSet("Group 1/", nil); err != nil { + t.Fatal(err) + } +} +func TestMove(t *testing.T) { + if err := Open(testDoc); err != nil { + t.Fatal(err) + } + d, err := ActiveDocument() + if err != nil { + t.Fatal(err) + } + lyr := d.LayerSet("Group 1").ArtLayer("Layer 1") + lyr.SetPos(100, 50, "TL") +} + +func TestActiveDocument(t *testing.T) { + if testing.Short() { + t.Skip("Skipping \"TestDocument\"") + } + Open(testDoc) + d, err := ActiveDocument() + defer func(){d.Dump()}() + if err != nil { + t.Fatal(err) + } + if d != d.artLayers[0].Parent() { + fmt.Println(d) + fmt.Println(d.artLayers[0].Parent()) + t.Fatal("ArtLayers do not have doc as parent.") + } + if d != d.layerSets[0].Parent() { + fmt.Println(d) + fmt.Println(d.layerSets[0].Parent()) + t.Fatal("LayerSets do not have doc as parent.") + } + if d.layerSets[0] != d.layerSets[0].artLayers[0].Parent() { + fmt.Println(d.layerSets[0]) + fmt.Println(d.layerSets[0].artLayers[0]) + fmt.Println(d.layerSets[0].artLayers[0].Parent()) + t.Fatal("Layerset's ArtLayers do not have correct parents") + } + lyr := d.LayerSet("Group 1").ArtLayer("Layer 1") + if lyr == nil { + t.Fatal("lyr does not exist") + } + s := Stroke{Size: 4, Color: &RGB{0, 0, 0}} + lyr.SetStroke(s, &RGB{128, 128, 128}) +} + +func TestColor(t *testing.T) { + byt, err := runner.Run("colorLayer.vbs", "255", "255", "255") + fmt.Println(string(byt)) + fmt.Println(err) + if err != nil { + + t.Fatal() + } +} + +func TestApplyDataset(t *testing.T) { + err := ApplyDataset("Anger") + if err != nil { + t.Fatal(err) + } +} + +func TestDocumentLayerSet(t *testing.T) { + if testing.Short() { + t.Skip("Skipping TestDocumentLayerSet") + } + d, err := ActiveDocument() + if err != nil { + t.Fatal(err) + } + set := d.LayerSet("Group 1") + fmt.Println(set) + for _, lyr := range set.ArtLayers() { + fmt.Println(lyr.name) + } + lyr := set.ArtLayer("Layer 1") + fmt.Println(lyr) + // set = d.LayerSet("Indicators").LayerSet("Life") + // fmt.Println(set) + for _, lyr := range set.ArtLayers() { + fmt.Println(lyr.name) + } +} + +func TestLoadedDoc(t *testing.T) { + var d *Document + byt, err := ioutil.ReadFile("./data/Test.psd.txt") + if err != nil { + t.Fatal(err) + } + err = json.Unmarshal(byt, &d) + if err != nil { + t.Fatal(err) + } + if d != d.ArtLayers()[0].Parent() { + t.Fatal("Loaded document's ArtLayers do not point to doc") + } + if d != d.LayerSets()[0].Parent() { + t.Fatal("Loaded document's LayerSets do not point to doc") + } + if d.LayerSets()[0] != d.layerSets[0].artLayers[0].Parent() { + t.Fatal("Loaded document's LayerSet's ArtLayers do not point to layerSets") + } +} + +func TestJSLayer(t *testing.T) { + d, _ := ActiveDocument() + set := d.LayerSet("Group 1") + lyr := set.ArtLayer("Layer 1") + fmt.Println(JSLayer(set.Path())) + fmt.Println(JSLayer(lyr.Path())) +} +func TestDoJs_HideLayer(t *testing.T) { + err := Open(testDoc) + if err != nil { + t.Fatal(err) + } + d, err := ActiveDocument() + if err != nil { + t.Fatal(err) + } + lyr, err := NewLayerSet("Group 1/", d) + lyr.SetVisible(false) + if err != nil { + t.Fatal(err) + } +} + +func TestTextItem(t *testing.T) { + // err := Open(testDoc) + // if err != nil { + // t.Fatal(err) + // } + + d, err := ActiveDocument() + if err != nil { + t.Fatal(err) + } + for _, lyr := range d.ArtLayers() { + if lyr.Name() == "Text" { + lyr.SetText("Butts") + // lyr.FmtText(0, 5, "Arial", "Regular") + // lyr.FmtText(0, 3, "Arial", "Bold") + } + } + + byt := []byte(`{"Name": "lyr", "TextItem": {"Contents": "lyr", "Size": 12.000, "Font": "ArialItalic"}}`) + lyr := &ArtLayer{} + // byt := []byte(`{"Name": "lyr"}`) + // lyr := &TextItem{} + err := lyr.UnmarshalJSON(byt) + fmt.Printf("%+v\n", lyr) + fmt.Println(lyr.TextItem) + if err != nil { + t.Fatal(err) + } +} +*/ +func BenchmarkDoc_Go(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := ActiveDocument() + if err != nil { + b.Fatal(err) + } + } +} + +//.8s +//.15 +func BenchmarkHideLayer(b *testing.B) { + for i := 0; i < b.N; i++ { + // _, err := Layers("Areas/TitleBackground/") + // if err != nil { + // b.Fatal(err) + // } + } +} + +// 59ns +func BenchmarkHelloWorld_go(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = fmt.Sprintf("Hello, world!") + } +} + +// ~35200000ns (.0352s) +func BenchmarkHelloWorld_vbs(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := runner.Run("helloworld") + if err != nil { + b.Fatal(err) + } + } +} + +// ~51700000 (0.0517) +func BenchmarkHelloWorld_js(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := DoJS("test.jsx", "Hello, World!") + if err != nil { + b.Fatal(err) + } + } +} diff --git a/runner/README.md b/runner/README.md new file mode 100644 index 0000000..ae56712 --- /dev/null +++ b/runner/README.md @@ -0,0 +1,35 @@ +# runner +[![GoDoc](https://godoc.org/github.com/sbrow/ps/runner?status.svg)](https://godoc.org/github.com/sbrow/runner) [![Build Status](https://travis-ci.org/sbrow/runner.svg?branch=master)](https://travis-ci.org/sbrow/runner) [![Coverage Status](https://coveralls.io/repos/github/sbrow/runner/badge.svg?branch=master)](https://coveralls.io/github/sbrow/runner?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/sbrow/ps/runner)](https://goreportcard.com/report/github.com/sbrow/ps/runner) + +`import "github.com/sbrow/ps/runner"` + +* [Overview](#pkg-overview) +* [Installation](pkg-installation) +* [Documentation](#pkg-doc) + +## Overview +Package runner runs the non-go code that Photoshop understands, +and passes it to back to the go program. Currently, this is +primarily implemented through Adobe Extendscript, but hopefully +in the future it will be upgraded to a C++ plugin. + + + + + +## Installation +```sh +$ go get -u github.com/sbrow/ps/runner +``` + + + +## Documentation +For full Documentation please visit https://godoc.org/github.com/sbrow/ps/runner +- - - + + +Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md) diff --git a/runner/runner.go b/runner/runner.go new file mode 100644 index 0000000..c50aed2 --- /dev/null +++ b/runner/runner.go @@ -0,0 +1,74 @@ +//go:generate sh -c "godoc2md -template ../.doc.template github.com/sbrow/ps/runner > README.md" + +// Package runner runs the non-go code that Photoshop understands, +// and passes it to back to the go program. Currently, this is +// primarily implemented through Adobe Extendscript, but hopefully +// in the future it will be upgraded to a C++ plugin. +package runner + +import ( + "bytes" + "fmt" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +// Windows is the runner Windows Operating Systems. +// It runs Visual Basic Scripts. +var Windows = Runner{ + Cmd: "cscript.exe", + Args: []string{"/nologo"}, + Ext: ".vbs", +} + +// pkgpath is the path to this package. +var pkgpath string + +// std is the Standard Runner. +var std Runner + +func init() { + _, file, _, _ := runtime.Caller(0) + pkgpath = filepath.Dir(file) + switch runtime.GOOS { + case "windows": + std = Windows + } +} + +// Runner runs script files to communicate between the OS/Photoshop and the Go code. +type Runner struct { + Cmd string // The name of the command to run + Args []string // The arguments to pass to the command. + Ext string // The file extension to use for these commands. +} + +// Run runs the standard runner with the given values. +func Run(name string, args ...string) ([]byte, error) { + var out, errs bytes.Buffer + cmd := exec.Command(std.Cmd, parseArgs(name, args...)...) + cmd.Stdout, cmd.Stderr = &out, &errs + if err := cmd.Run(); err != nil || len(errs.Bytes()) != 0 { + return out.Bytes(), fmt.Errorf(`err: "%s" +errs.String(): "%s" +args: "%s" +out: "%s"`, err, errs.String(), args, out.String()) + } + return out.Bytes(), nil +} + +// parseArgs parses the given args into the correct syntax. +func parseArgs(name string, args ...string) []string { + if !strings.HasSuffix(name, std.Ext) { + name += std.Ext + } + newArgs := append(std.Args, filepath.Join(pkgpath, "scripts", name)) + if strings.Contains(name, "dojs") { + newArgs = append(newArgs, args[0], fmt.Sprint(strings.Join(args[1:], ",,"))) + } else { + newArgs = append(newArgs, args...) + } + return newArgs +} diff --git a/runner/runner_test.go b/runner/runner_test.go new file mode 100644 index 0000000..95be21d --- /dev/null +++ b/runner/runner_test.go @@ -0,0 +1,55 @@ +package runner + +import ( + "io/ioutil" + "log" + "os" + "path/filepath" + "runtime" + "testing" +) + +var scripts string + +func init() { + var ok bool + _, file, _, ok := runtime.Caller(0) + if !ok { + log.Panic("package path not found") + } + pkgpath = filepath.Dir(file) + scripts = filepath.Join(pkgpath, "scripts") +} +func TestRun(t *testing.T) { + tests := []struct { + name string + args []string + want []byte + wantErr bool + }{ + {"dojs", []string{filepath.Join(scripts, "test.jsx"), filepath.Join(scripts, "test.txt"), "arg1", "arg2"}, + []byte(filepath.Join(scripts, "test.txt") + "\r\narg1\r\narg2\r\n"), false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := Run(tt.name, tt.args...) + if (err != nil) != tt.wantErr { + t.Errorf("Run() error = %v, wantErr %v", err, tt.wantErr) + return + } + f, err := os.Open(tt.args[1]) + if err != nil { + t.Error(err) + return + } + got, err := ioutil.ReadAll(f) + if err != nil { + t.Error(err) + return + } + if string(got) != string(tt.want) { + t.Errorf("Run() = \n%v, want \n%v", string(got), string(tt.want)) + } + }) + } +} diff --git a/runner/scripts/PsIsOpen.vbs b/runner/scripts/PsIsOpen.vbs new file mode 100644 index 0000000..2e44e2f --- /dev/null +++ b/runner/scripts/PsIsOpen.vbs @@ -0,0 +1,12 @@ +Function IsProcessRunning( strComputer, strProcess ) + Dim Process, strObject + IsProcessRunning = False + strObject = "winmgmts://" & strComputer + For Each Process in GetObject( strObject ).InstancesOf( "win32_process" ) + If UCase( Process.name ) = UCase( strProcess ) Then + IsProcessRunning = True + Exit Function + End If + Next +End Function +wScript.Echo IsProcessRunning(".", "Photoshop.exe") diff --git a/runner/scripts/SetName.jsx b/runner/scripts/SetName.jsx new file mode 100644 index 0000000..0bf2083 --- /dev/null +++ b/runner/scripts/SetName.jsx @@ -0,0 +1,5 @@ +#include lib.js +var stdout=newFile(arguments[0]); +var obj=eval(arguments[1]); +obj.name=arguments[2]; +stdout.close(); \ No newline at end of file diff --git a/runner/scripts/action.vbs b/runner/scripts/action.vbs new file mode 100644 index 0000000..70cd5de --- /dev/null +++ b/runner/scripts/action.vbs @@ -0,0 +1,11 @@ +' Runs an action with the given name (Argument 1) from the given set (Argument 0). +set appRef = CreateObject("Photoshop.Application") +' No dialogs. +dlgMode = 3 + +set desc = CreateObject( "Photoshop.ActionDescriptor" ) +set ref = CreateObject( "Photoshop.ActionReference" ) +Call ref.PutName(appRef.CharIDToTypeID("Actn"), wScript.Arguments(1)) +Call ref.PutName(appRef.CharIDToTypeID("ASet"), wScript.Arguments(0)) +Call desc.PutReference(appRef.CharIDToTypeID("null"), ref) +Call appRef.ExecuteAction(appRef.CharIDToTypeID("Ply "), desc, dlgMode) \ No newline at end of file diff --git a/runner/scripts/activeDocName.jsx b/runner/scripts/activeDocName.jsx new file mode 100644 index 0000000..ef8543b --- /dev/null +++ b/runner/scripts/activeDocName.jsx @@ -0,0 +1,2 @@ +#include lib.js +var stdout = newFile(arguments[0]);stdout.writeln(app.activeDocument.name);stdout.close(); \ No newline at end of file diff --git a/runner/scripts/applyDataset.jsx b/runner/scripts/applyDataset.jsx new file mode 100644 index 0000000..2e35e13 --- /dev/null +++ b/runner/scripts/applyDataset.jsx @@ -0,0 +1,15 @@ +var saveFile = File(arguments[0]); +if(saveFile.exists) + saveFile.remove(); +var idAply = charIDToTypeID("Aply"); +var desc1 = new ActionDescriptor(); +var idnull = charIDToTypeID("null"); +var ref1 = new ActionReference(); +var iddataSetClass = stringIDToTypeID("dataSetClass"); +ref1.putName(iddataSetClass, arguments[1]); +desc1.putReference(idnull, ref1); +executeAction(idAply, desc1, DialogModes.NO); +saveFile.encoding = "UTF8"; +saveFile.open("e", "TEXT", "????"); +saveFile.writeln("done!"); +saveFile.close(); \ No newline at end of file diff --git a/runner/scripts/close.vbs b/runner/scripts/close.vbs new file mode 100644 index 0000000..ac2275f --- /dev/null +++ b/runner/scripts/close.vbs @@ -0,0 +1,3 @@ +set App = CreateObject("Photoshop.Application") +set Doc = App.activeDocument +Doc.Close(CInt(wScript.Arguments(0))) \ No newline at end of file diff --git a/runner/scripts/colorLayer.vbs b/runner/scripts/colorLayer.vbs new file mode 100644 index 0000000..cd5dc29 --- /dev/null +++ b/runner/scripts/colorLayer.vbs @@ -0,0 +1,76 @@ +DIM objApp +SET objApp = CreateObject("Photoshop.Application") +DIM dialogMode +dialogMode = 3 +DIM idsetd +idsetd = objApp.CharIDToTypeID("setd") + DIM desc134 + SET desc134 = CreateObject("Photoshop.ActionDescriptor") + DIM idnull + idnull = objApp.CharIDToTypeID("null") + DIM ref44 + SET ref44 = CreateObject("Photoshop.ActionReference") + DIM idPrpr + idPrpr = objApp.CharIDToTypeID("Prpr") + DIM idLefx + idLefx = objApp.CharIDToTypeID("Lefx") + Call ref44.PutProperty(idPrpr, idLefx) + DIM idLyr + idLyr = objApp.CharIDToTypeID("Lyr ") + DIM idOrdn + idOrdn = objApp.CharIDToTypeID("Ordn") + DIM idTrgt + idTrgt = objApp.CharIDToTypeID("Trgt") + Call ref44.PutEnumerated(idLyr, idOrdn, idTrgt) + Call desc134.PutReference(idnull, ref44) + DIM idT + idT = objApp.CharIDToTypeID("T ") + DIM desc135 + SET desc135 = CreateObject("Photoshop.ActionDescriptor") + DIM idScl + idScl = objApp.CharIDToTypeID("Scl ") + DIM idPrc + idPrc = objApp.CharIDToTypeID("#Prc") + Call desc135.PutUnitDouble(idScl, idPrc, 416.666667) + DIM idSoFi + idSoFi = objApp.CharIDToTypeID("SoFi") + DIM desc136 + SET desc136 = CreateObject("Photoshop.ActionDescriptor") + DIM idenab + idenab = objApp.CharIDToTypeID("enab") + Call desc136.PutBoolean(idenab, True) + DIM idMd + idMd = objApp.CharIDToTypeID("Md ") + DIM idBlnM + idBlnM = objApp.CharIDToTypeID("BlnM") + DIM idNrml + idNrml = objApp.CharIDToTypeID("Nrml") + Call desc136.PutEnumerated(idMd, idBlnM, idNrml) + DIM idOpct + idOpct = objApp.CharIDToTypeID("Opct") + idPrc = objApp.CharIDToTypeID("#Prc") + Call desc136.PutUnitDouble(idOpct, idPrc, 100.000000) + DIM idClr + idClr = objApp.CharIDToTypeID("Clr ") + DIM desc137 + SET desc137 = CreateObject("Photoshop.ActionDescriptor") + DIM idRd + idRd = objApp.CharIDToTypeID("Rd ") + Call desc137.PutDouble(idRd, CInt(wScript.Arguments(0))) + ' Call desc137.PutDouble(idRd, 255) + DIM idGrn + idGrn = objApp.CharIDToTypeID("Grn ") + Call desc137.PutDouble(idGrn, Cint(wScript.Arguments(1))) + ' Call desc137.PutDouble(idGrn, 255) + DIM idBl + idBl = objApp.CharIDToTypeID("Bl ") + Call desc137.PutDouble(idBl, CInt(wScript.Arguments(2))) + ' Call desc137.PutDouble(idBl, 255) + DIM idRGBC + idRGBC = objApp.CharIDToTypeID("RGBC") + Call desc136.PutObject(idClr, idRGBC, desc137) + idSoFi = objApp.CharIDToTypeID("SoFi") + Call desc135.PutObject(idSoFi, idSoFi, desc136) + idLefx = objApp.CharIDToTypeID("Lefx") + Call desc134.PutObject(idT, idLefx, desc135) +Call objApp.ExecuteAction(idsetd, desc134, dialogMode) \ No newline at end of file diff --git a/runner/scripts/colorStroke.vbs b/runner/scripts/colorStroke.vbs new file mode 100644 index 0000000..b2b1f92 --- /dev/null +++ b/runner/scripts/colorStroke.vbs @@ -0,0 +1,120 @@ +DIM objApp +SET objApp = CreateObject("Photoshop.Application") +REM Use dialog mode 3 for show no dialogs +DIM dialogMode +dialogMode = 3 +DIM idsetd +idsetd = objApp.CharIDToTypeID("setd") + DIM desc2 + SET desc2 = CreateObject("Photoshop.ActionDescriptor") + DIM idnull + idnull = objApp.CharIDToTypeID("null") + DIM ref2 + SET ref2 = CreateObject("Photoshop.ActionReference") + DIM idPrpr + idPrpr = objApp.CharIDToTypeID("Prpr") + DIM idLefx + idLefx = objApp.CharIDToTypeID("Lefx") + Call ref2.PutProperty(idPrpr, idLefx) + DIM idLyr + idLyr = objApp.CharIDToTypeID("Lyr ") + DIM idOrdn + idOrdn = objApp.CharIDToTypeID("Ordn") + DIM idTrgt + idTrgt = objApp.CharIDToTypeID("Trgt") + Call ref2.PutEnumerated(idLyr, idOrdn, idTrgt) + Call desc2.PutReference(idnull, ref2) + DIM idT + idT = objApp.CharIDToTypeID("T ") + DIM desc3 + SET desc3 = CreateObject("Photoshop.ActionDescriptor") + DIM idScl + idScl = objApp.CharIDToTypeID("Scl ") + DIM idPrc + idPrc = objApp.CharIDToTypeID("#Prc") + Call desc3.PutUnitDouble(idScl, idPrc, 416.666667) + DIM idSoFi + idSoFi = objApp.CharIDToTypeID("SoFi") + DIM desc4 + SET desc4 = CreateObject("Photoshop.ActionDescriptor") + DIM idenab + idenab = objApp.CharIDToTypeID("enab") + Call desc4.PutBoolean(idenab, True) + DIM idMd + idMd = objApp.CharIDToTypeID("Md ") + DIM idBlnM + idBlnM = objApp.CharIDToTypeID("BlnM") + DIM idNrml + idNrml = objApp.CharIDToTypeID("Nrml") + Call desc4.PutEnumerated(idMd, idBlnM, idNrml) + DIM idOpct + idOpct = objApp.CharIDToTypeID("Opct") + idPrc = objApp.CharIDToTypeID("#Prc") + Call desc4.PutUnitDouble(idOpct, idPrc, 100.000000) + DIM idClr + idClr = objApp.CharIDToTypeID("Clr ") + DIM desc5 + SET desc5 = CreateObject("Photoshop.ActionDescriptor") + DIM idRd + idRd = objApp.CharIDToTypeID("Rd ") + Call desc5.PutDouble(idRd, CInt(wScript.Arguments(0))) + DIM idGrn + idGrn = objApp.CharIDToTypeID("Grn ") + Call desc5.PutDouble(idGrn,CInt(wScript.Arguments(1))) + DIM idBl + idBl = objApp.CharIDToTypeID("Bl ") + Call desc5.PutDouble(idBl, CInt(wScript.Arguments(2))) + DIM idRGBC + idRGBC = objApp.CharIDToTypeID("RGBC") + Call desc4.PutObject(idClr, idRGBC, desc5) + idSoFi = objApp.CharIDToTypeID("SoFi") + Call desc3.PutObject(idSoFi, idSoFi, desc4) + DIM idFrFX + idFrFX = objApp.CharIDToTypeID("FrFX") + DIM desc6 + SET desc6 = CreateObject("Photoshop.ActionDescriptor") + idenab = objApp.CharIDToTypeID("enab") + Call desc6.PutBoolean(idenab, True) + DIM idStyl + idStyl = objApp.CharIDToTypeID("Styl") + DIM idFStl + idFStl = objApp.CharIDToTypeID("FStl") + DIM idOutF + idOutF = objApp.CharIDToTypeID("OutF") + Call desc6.PutEnumerated(idStyl, idFStl, idOutF) + DIM idPntT + idPntT = objApp.CharIDToTypeID("PntT") + DIM idFrFl + idFrFl = objApp.CharIDToTypeID("FrFl") + DIM idSClr + idSClr = objApp.CharIDToTypeID("SClr") + Call desc6.PutEnumerated(idPntT, idFrFl, idSClr) + idMd = objApp.CharIDToTypeID("Md ") + idBlnM = objApp.CharIDToTypeID("BlnM") + idNrml = objApp.CharIDToTypeID("Nrml") + Call desc6.PutEnumerated(idMd, idBlnM, idNrml) + idOpct = objApp.CharIDToTypeID("Opct") + idPrc = objApp.CharIDToTypeID("#Prc") + Call desc6.PutUnitDouble(idOpct, idPrc, 100.000000) + DIM idSz + idSz = objApp.CharIDToTypeID("Sz ") + DIM idPxl + idPxl = objApp.CharIDToTypeID("#Pxl") + Call desc6.PutUnitDouble(idSz, idPxl, CLng(wScript.Arguments(3))) + idClr = objApp.CharIDToTypeID("Clr ") + DIM desc7 + SET desc7 = CreateObject("Photoshop.ActionDescriptor") + idRd = objApp.CharIDToTypeID("Rd ") + Call desc7.PutDouble(idRd, CInt(wScript.Arguments(4))) + idGrn = objApp.CharIDToTypeID("Grn ") + Call desc7.PutDouble(idGrn, CInt(wScript.Arguments(5))) + idBl = objApp.CharIDToTypeID("Bl ") + Call desc7.PutDouble(idBl, CInt(wScript.Arguments(6))) + idRGBC = objApp.CharIDToTypeID("RGBC") + Call desc6.PutObject(idClr, idRGBC, desc7) + idFrFX = objApp.CharIDToTypeID("FrFX") + Call desc3.PutObject(idFrFX, idFrFX, desc6) + idLefx = objApp.CharIDToTypeID("Lefx") + Call desc2.PutObject(idT, idLefx, desc3) +Call objApp.ExecuteAction(idsetd, desc2, dialogMode) + diff --git a/runner/scripts/compilejs.jsx b/runner/scripts/compilejs.jsx new file mode 100644 index 0000000..e732956 --- /dev/null +++ b/runner/scripts/compilejs.jsx @@ -0,0 +1,4 @@ +#include lib.js +var stdout = newFile(arguments[0]) +eval(arguments[1]); +stdout.close() \ No newline at end of file diff --git a/runner/scripts/dojs.vbs b/runner/scripts/dojs.vbs new file mode 100644 index 0000000..561832d --- /dev/null +++ b/runner/scripts/dojs.vbs @@ -0,0 +1,13 @@ + +Dim appRef +Set appRef = CreateObject("Photoshop.Application") +if wScript.Arguments.Count = 0 then + wScript.Echo "Missing parameters" +else + path = wScript.Arguments(0) + args = wScript.Arguments(1) + error = appRef.DoJavaScriptFile(path, Split(args, ",,")) + if Not error = "true" and Not error = "[ActionDescriptor]" and Not error = "undefined" Then + Err.raise 1, "dojs.vbs", error + end if +end if \ No newline at end of file diff --git a/runner/scripts/fmtText.jsx b/runner/scripts/fmtText.jsx new file mode 100644 index 0000000..1a1f408 --- /dev/null +++ b/runner/scripts/fmtText.jsx @@ -0,0 +1,54 @@ +if(app.activeDocument.activeLayer.kind == LayerKind.TEXT){ + var activeLayer = app.activeDocument.activeLayer; + if(activeLayer.kind == LayerKind.TEXT){ + var start = parseInt(arguments[1]); + var end = parseInt(arguments[2]); + var fontName = arguments[3]; + var fontStyle = arguments[4]; + var fontSize = activeLayer.textItem.size; + var colorArray = [0, 0, 0]; + if((activeLayer.textItem.contents != "")&&(start >= 0)&&(end <= activeLayer.textItem.contents.length)){ + var idsetd = app.charIDToTypeID( "setd" ); + var action = new ActionDescriptor(); + var idnull = app.charIDToTypeID( "null" ); + var reference = new ActionReference(); + var idTxLr = app.charIDToTypeID( "TxLr" ); + var idOrdn = app.charIDToTypeID( "Ordn" ); + var idTrgt = app.charIDToTypeID( "Trgt" ); + reference.putEnumerated( idTxLr, idOrdn, idTrgt ); + action.putReference( idnull, reference ); + var idT = app.charIDToTypeID( "T " ); + var textAction = new ActionDescriptor(); + var idTxtt = app.charIDToTypeID( "Txtt" ); + var actionList = new ActionList(); + var textRange = new ActionDescriptor(); + var idFrom = app.charIDToTypeID( "From" ); + textRange.putInteger( idFrom, start ); + textRange.putInteger( idT, end ); + var idTxtS = app.charIDToTypeID( "TxtS" ); + var formatting = new ActionDescriptor(); + var idFntN = app.charIDToTypeID( "FntN" ); + formatting.putString( idFntN, fontName ); + var idFntS = app.charIDToTypeID( "FntS" ); + formatting.putString( idFntS, fontStyle ); + var idSz = app.charIDToTypeID( "Sz " ); + var idPnt = app.charIDToTypeID( "#Pnt" ); + formatting.putUnitDouble( idSz, idPnt, fontSize ); + var idClr = app.charIDToTypeID( "Clr " ); + var colorAction = new ActionDescriptor(); + var idRd = app.charIDToTypeID( "Rd " ); + colorAction.putDouble( idRd, colorArray[0] ); + var idGrn = app.charIDToTypeID( "Grn " ); + colorAction.putDouble( idGrn, colorArray[1]); + var idBl = app.charIDToTypeID( "Bl " ); + colorAction.putDouble( idBl, colorArray[2] ); + var idRGBC = app.charIDToTypeID( "RGBC" ); + formatting.putObject( idClr, idRGBC, colorAction ); + textRange.putObject( idTxtS, idTxtS, formatting ); + actionList.putObject( idTxtt, textRange ); + textAction.putList( idTxtt, actionList ); + action.putObject( idT, idTxLr, textAction ); + app.executeAction( idsetd, action, DialogModes.NO ); + } + } +} diff --git a/runner/scripts/getActiveDoc.jsx b/runner/scripts/getActiveDoc.jsx new file mode 100644 index 0000000..9f8ff62 --- /dev/null +++ b/runner/scripts/getActiveDoc.jsx @@ -0,0 +1,22 @@ +#include lib.js +var stdout = newFile(arguments[0]); +var doc = app.activeDocument; +stdout.writeln(('{"Name": "'+doc.name+'", "FullName": "'+doc.fullName+'", "Height":'+doc.height+ + ', "Width":'+doc.width+', "ArtLayers": [').replace(/ px/g, "")); + +stdout.writeln(layers(doc.artLayers)) +stdout.writeln('], "LayerSets": ['); +function lyrSets(sets, nm) { + if (typeof sets === 'undefined') + return; + for (var i = 0; i < sets.length; i++) { + var set = sets[i]; + var name = nm + set.name + "/"; + stdout.write('{"Name": "' + set.name + '", "Visible":'+ set.visible +'}'); + if (i+1 != sets.length) + stdout.write(','); + } +} +lyrSets(doc.layerSets) +stdout.write(']}'); +stdout.close(); \ No newline at end of file diff --git a/runner/scripts/getLayer.jsx b/runner/scripts/getLayer.jsx new file mode 100644 index 0000000..6bbfeca --- /dev/null +++ b/runner/scripts/getLayer.jsx @@ -0,0 +1,6 @@ +#include lib.js +app.displayDialogs=DialogModes.NO +var stdout = newFile(arguments[0]); +var lyr = eval(arguments[1]); +var lyrs = [lyr]; +stdout.writeln(layers(lyrs)) \ No newline at end of file diff --git a/runner/scripts/getLayerSet.jsx b/runner/scripts/getLayerSet.jsx new file mode 100644 index 0000000..694f263 --- /dev/null +++ b/runner/scripts/getLayerSet.jsx @@ -0,0 +1,21 @@ +#include lib.js +var stdout = newFile(arguments[0]); +var set = eval(arguments[1]); +stdout.writeln('{"Name": "'+set.name+'", "Visible": '+ set.visible +', "ArtLayers":['); +stdout.flush(); +var str = layers(set.artLayers); +str = str.replace(/\r/g, "\\r"); +stdout.writeln(str); +stdout.writeln("]"); +stdout.write(', "LayerSets": [') +for (var i = 0; i < set.layerSets.length; i++) { + var s = set.layerSets[i]; + stdout.write('{"Name":"' + s.name + '", "Visible": ' + s.visible + '}'); + if (i < set.layerSets.length - 1) + stdout.writeln(","); + stdout.flush() +} +stdout.writeln(']') +stdout.write(', "Bounds": [[],[]]'); +stdout.write("}"); +stdout.close(); \ No newline at end of file diff --git a/runner/scripts/isVisible.jsx b/runner/scripts/isVisible.jsx new file mode 100644 index 0000000..0e926a8 --- /dev/null +++ b/runner/scripts/isVisible.jsx @@ -0,0 +1,2 @@ +#include lib.js +var stdout = newFile(arguments[0]);stdout.writeln(eval(arguments[1]).visible);stdout.close(); \ No newline at end of file diff --git a/runner/scripts/layerSetBounds.jsx b/runner/scripts/layerSetBounds.jsx new file mode 100644 index 0000000..b1def30 --- /dev/null +++ b/runner/scripts/layerSetBounds.jsx @@ -0,0 +1,11 @@ +#include lib.js +var stdout = newFile(arguments[0]); +var set = eval(arguments[1]); +app.activeDocument.activeLayer=set; +set.merge(); +set=eval(arguments[2]); +stdout.write(('[[' + set.bounds[0] + ',' + + set.bounds[1] + '],[' + set.bounds[2] + ',' + + set.bounds[3] + ']]').replace(/ px/g, "")); +Undo(); +stdout.close(); \ No newline at end of file diff --git a/runner/scripts/lib.js b/runner/scripts/lib.js new file mode 100644 index 0000000..25401b7 --- /dev/null +++ b/runner/scripts/lib.js @@ -0,0 +1,122 @@ +// Opens and returns a file, overwriting new data. +function newFile(path) { + var f = File(path) + f.encoding = "UTF8" + f.open("w") + return f +} + +File.prototype.flush = function() { + this.close() + this.open("a") +}; +function flush(file) { + file.close() + file.open("a") +} + + +// Prints an error message. +function err(e) { + return 'ERROR: ' + e.message + ' at ' + e.fileName + ':' + e.line; +} + +function bounds(lyr) { + return ('"Bounds": [[' + lyr.bounds[0] + ',' + + lyr.bounds[1] + '],[' + lyr.bounds[2] + ',' + + lyr.bounds[3] + ']]').replace(/ px/g, ""); +} + +function Undo() { + var desc = new ActionDescriptor(); + var ref = new ActionReference(); + ref.putEnumerated( charIDToTypeID( "HstS" ), charIDToTypeID( "Ordn" ), charIDToTypeID( "Prvs" )); + desc.putReference(charIDToTypeID( "null" ), ref); + executeAction( charIDToTypeID( "slct" ), desc, DialogModes.NO ); +} + +/** +* The setFormatting function sets the font, font style, point size, and RGB color of specified +* characters in a Photoshop text layer. +* +* @param start (int) the index of the insertion point *before* the character you want., +* @param end (int) the index of the insertion point following the character. +* @param fontName is a string for the font name. +* @param fontStyle is a string for the font style. +* @param fontSize (Number) the point size of the text. +* @param colorArray (Array) is the RGB color to be applied to the text. +*/ +function setFormatting(start, end, fontName, fontStyle, fontSize, colorArray) { + if(app.activeDocument.activeLayer.kind == LayerKind.TEXT){ + var activeLayer = app.activeDocument.activeLayer; + fontSize = activeLayer.textItem.size; + colorArray = [0, 0, 0]; + if(activeLayer.kind == LayerKind.TEXT){ + if((activeLayer.textItem.contents != "")&&(start >= 0)&&(end <= activeLayer.textItem.contents.length)){ + var idsetd = app.charIDToTypeID( "setd" ); + var action = new ActionDescriptor(); + var idnull = app.charIDToTypeID( "null" ); + var reference = new ActionReference(); + var idTxLr = app.charIDToTypeID( "TxLr" ); + var idOrdn = app.charIDToTypeID( "Ordn" ); + var idTrgt = app.charIDToTypeID( "Trgt" ); + reference.putEnumerated( idTxLr, idOrdn, idTrgt ); + action.putReference( idnull, reference ); + var idT = app.charIDToTypeID( "T " ); + var textAction = new ActionDescriptor(); + var idTxtt = app.charIDToTypeID( "Txtt" ); + var actionList = new ActionList(); + var textRange = new ActionDescriptor(); + var idFrom = app.charIDToTypeID( "From" ); + textRange.putInteger( idFrom, start ); + textRange.putInteger( idT, end ); + var idTxtS = app.charIDToTypeID( "TxtS" ); + var formatting = new ActionDescriptor(); + var idFntN = app.charIDToTypeID( "FntN" ); + formatting.putString( idFntN, fontName ); + var idFntS = app.charIDToTypeID( "FntS" ); + formatting.putString( idFntS, fontStyle ); + var idSz = app.charIDToTypeID( "Sz " ); + var idPnt = app.charIDToTypeID( "#Pnt" ); + formatting.putUnitDouble( idSz, idPnt, fontSize ); + var idClr = app.charIDToTypeID( "Clr " ); + var colorAction = new ActionDescriptor(); + var idRd = app.charIDToTypeID( "Rd " ); + colorAction.putDouble( idRd, colorArray[0] ); + var idGrn = app.charIDToTypeID( "Grn " ); + colorAction.putDouble( idGrn, colorArray[1]); + var idBl = app.charIDToTypeID( "Bl " ); + colorAction.putDouble( idBl, colorArray[2] ); + var idRGBC = app.charIDToTypeID( "RGBC" ); + formatting.putObject( idClr, idRGBC, colorAction ); + textRange.putObject( idTxtS, idTxtS, formatting ); + actionList.putObject( idTxtt, textRange ); + textAction.putList( idTxtt, actionList ); + action.putObject( idT, idTxLr, textAction ); + app.executeAction( idsetd, action, DialogModes.NO ); + } + } + } +} + +function layers(lyrs) { + if (typeof lyrs === 'undefined') + return; + var str = ""; + for (var i = 0; i < lyrs.length; i++) { + var lyr = lyrs[i]; + str += ('{"Name":"' + lyr.name + '", "Bounds": [[' + lyr.bounds[0] + ',' + + lyr.bounds[1] + '],[' + lyr.bounds[2] + ',' + + lyr.bounds[3] + ']], "Visible": ' + lyr.visible+', "TextItem": ').replace(/ px/g, ""); + if (lyr.kind == LayerKind.TEXT) { + str += ('{"Contents": "'+lyr.textItem.contents+'",').replace(/\r/g, '\\r'); + str += (' "Size": '+lyr.textItem.size+',').replace(/ pt/g, ''); + str += ' "Font": "'+lyr.textItem.font+'"}\n' + } else + str += "null"; + str += "}"; + if (i+1 != lyrs.length) + str += ','; + } + return str +} \ No newline at end of file diff --git a/runner/scripts/moveLayer.jsx b/runner/scripts/moveLayer.jsx new file mode 100644 index 0000000..12187eb --- /dev/null +++ b/runner/scripts/moveLayer.jsx @@ -0,0 +1,11 @@ +#include lib.js +var stdout = newFile(arguments[0]); +var lyr = eval(arguments[1]); +lyr.translate((Number)(arguments[2]), (Number)(arguments[3])); +if (lyr.typename == 'LayerSet') { + lyr.merge() + lyr=eval(arguments[4]) + Undo(); +} +stdout.writeln('{' + bounds(lyr) + '}') +stdout.close(); \ No newline at end of file diff --git a/runner/scripts/open.vbs b/runner/scripts/open.vbs new file mode 100644 index 0000000..f0c4440 --- /dev/null +++ b/runner/scripts/open.vbs @@ -0,0 +1,9 @@ +' Open photoshop. +Set app = CreateObject("Photoshop.Application") +if WScript.Arguments.Count = 0 then + WScript.Echo "Missing parameters" +else +path = wScript.Arguments(0) +Set doc = app.Open(path) +wScript.echo doc.Name +end if \ No newline at end of file diff --git a/runner/scripts/quit.vbs b/runner/scripts/quit.vbs new file mode 100644 index 0000000..9f77998 --- /dev/null +++ b/runner/scripts/quit.vbs @@ -0,0 +1,8 @@ +' Close Photoshop +Set appRef = CreateObject("Photoshop.Application") + +Do While appRef.Documents.Count > 0 + appRef.ActiveDocument.Close(CInt(wScript.Arguments(0))) +Loop + +appRef.Quit() \ No newline at end of file diff --git a/runner/scripts/save.vbs b/runner/scripts/save.vbs new file mode 100644 index 0000000..e23f1ac --- /dev/null +++ b/runner/scripts/save.vbs @@ -0,0 +1,12 @@ +Set appRef = CreateObject("Photoshop.Application") +dlgMode = 3 'No dialog +set d = CreateObject( "Photoshop.ActionDescriptor" ) +Call d.PutEnumerated(appRef.CharIDToTypeID("PGIT"), appRef.CharIDToTypeID("PGIT"), appRef.CharIDToTypeID("PGIN")) +Call d.PutEnumerated(appRef.CharIDToTypeID("PNGf"), appRef.CharIDToTypeID("PNGf"), appRef.CharIDToTypeID("PGAd")) + +SET desc = CreateObject( "Photoshop.ActionDescriptor" ) +Call desc.PutObject( appRef.CharIDToTypeID("As "), appRef.CharIDToTypeID("PNGF"), d) +Call desc.PutPath( appRef.CharIDToTypeID("In "), wScript.Arguments(0)) +Call desc.PutBoolean( appRef.CharIDToTypeID("Cpy "), True ) + +Call appRef.ExecuteAction(appRef.CharIDToTypeID("save"), desc, dlgMode) \ No newline at end of file diff --git a/runner/scripts/start.vbs b/runner/scripts/start.vbs new file mode 100644 index 0000000..3b13c62 --- /dev/null +++ b/runner/scripts/start.vbs @@ -0,0 +1 @@ +set app = CreateObject("Photoshop.Application") \ No newline at end of file diff --git a/runner/scripts/test.jsx b/runner/scripts/test.jsx new file mode 100644 index 0000000..280da5f --- /dev/null +++ b/runner/scripts/test.jsx @@ -0,0 +1,6 @@ +#include lib.js +var f = newFile(arguments[0]); +for (var i = 0; i < arguments.length; i++) { + f.writeln(arguments[i]); +} +f.close(); \ No newline at end of file diff --git a/runner/scripts/test.txt b/runner/scripts/test.txt new file mode 100644 index 0000000..5e17173 --- /dev/null +++ b/runner/scripts/test.txt @@ -0,0 +1,3 @@ +C:\Users\Spencer\go\src\github.com\sbrow\ps\runner\scripts\test.txt +arg1 +arg2 diff --git a/runner/scripts/test.vbs b/runner/scripts/test.vbs new file mode 100644 index 0000000..b413227 --- /dev/null +++ b/runner/scripts/test.vbs @@ -0,0 +1,3 @@ +for i=0 to wScript.Arguments.length-1 + wScript.echo wScript.Arguments(i) +next \ No newline at end of file diff --git a/textlayer.go b/textlayer.go new file mode 100644 index 0000000..8c8d6db --- /dev/null +++ b/textlayer.go @@ -0,0 +1,107 @@ +package ps + +import ( + "encoding/json" + "fmt" + "log" +) + +// TextItem holds the text element of a TextLayer. +type TextItem struct { + contents string + size float64 + font string + parent *ArtLayer +} + +// TextItemJSON is the exported version of TextItem +// that allows it to be marshaled and unmarshaled +// into JSON. +type TextItemJSON struct { + Contents string + Size float64 + Font string +} + +// Contents returns the raw text of the TextItem. +func (t TextItem) Contents() string { + return t.contents +} + +// Size returns the font size of the TextItem +func (t TextItem) Size() float64 { + return t.size +} + +// MarshalJSON implements the json.Marshaler interface, allowing the TextItem to be +// saved to disk in JSON format. +func (t *TextItem) MarshalJSON() ([]byte, error) { + return json.Marshal(&TextItemJSON{ + Contents: t.contents, + Size: t.size, + Font: t.font, + }) +} + +// UnmarshalJSON loads the JSON data into the TextItem +func (t *TextItem) UnmarshalJSON(data []byte) error { + tmp := &TextItemJSON{} + if err := json.Unmarshal(data, &tmp); err != nil { + return err + } + t.contents = tmp.Contents + t.size = tmp.Size + t.font = tmp.Font + return nil +} + +// SetText sets the text to the given string. +func (t *TextItem) SetText(txt string) { + if txt == t.contents { + return + } + var err error + lyr := JSLayer(t.parent.Path()) + bndtext := "[[' + lyr.bounds[0] + ',' + lyr.bounds[1] + '],[' + lyr.bounds[2] + ',' + lyr.bounds[3] + ']]" + js := fmt.Sprintf(`%s.textItem.contents='%s';var lyr = %[1]s;stdout.writeln(('%[3]s').replace(/ px/g, ''));`, + lyr, txt, bndtext) + var byt []byte + if byt, err = DoJS("compilejs.jsx", js); err != nil { + log.Panic(err) + } + var bnds *[2][2]int + err = json.Unmarshal(byt, &bnds) + if err != nil || bnds == nil { + log.Println("text:", txt) + log.Println("js:", js) + fmt.Printf("byt: '%s'\n", string(byt)) + log.Panic(err) + } + t.contents = txt + t.parent.bounds = *bnds +} + +// SetSize sets the size of the TextItem's font. +func (t *TextItem) SetSize(s float64) { + if t.size == s { + return + } + lyr := JSLayer(t.parent.Path()) + js := fmt.Sprintf("%s.textItem.size=%f;", lyr, s) + _, err := DoJS("compilejs.jsx", js) + if err != nil { + t.size = s + } +} + +// Fmt applies the given font and style to all characters +// in the range [start, end]. +func (t *TextItem) Fmt(start, end int, font, style string) { + if !t.parent.Visible() { + return + } + _, err := DoJS("fmtText.jsx", fmt.Sprint(start), fmt.Sprint(end), font, style) + if err != nil { + log.Panic(err) + } +}