├── .gitignore
├── psdparse
├── __init__.py
└── psdparser.py
├── README.txt
├── setup.py
├── bin
└── psdparser.py
└── LICENSE.txt
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | build/
3 |
--------------------------------------------------------------------------------
/psdparse/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | # This file is part of psdparser.
4 | #
5 | # Psdparser is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # Psdparser is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with Psdparser. If not, see .
17 |
18 | from psdparser import *
19 |
--------------------------------------------------------------------------------
/README.txt:
--------------------------------------------------------------------------------
1 | Original name: psdparse
2 | Original author: Copyright (C) 2004-6 Toby Thain, toby@telegraphics.com.au
3 |
4 | Forked in pure Python with the name of "psdparser"
5 | Copyright (C) 2008-10 Jérémy Bethmont, jeremy.bethmont@gmail.com
6 |
7 | This utility parses and prints a description of various structures
8 | inside an Adobe Photoshop(TM) PSD format file.
9 | It can optionally extract raster layers and spot/alpha channels to PNG files.
10 |
11 | A reasonable amount of integrity checking is performed. Corrupt images may
12 | still cause the program to give up, but it is usually much more robust
13 | than Photoshop when dealing with damaged files: It is unlikely to crash,
14 | and it recovers a more complete image.
15 |
16 | Tested with PSDs created by PS 3.0, 5.5, 7.0, CS and CS2,
17 | in Bitmap, Indexed, Grey Scale, CMYK and RGB Colour modes
18 | and 8/16 bit depths, with up to 53 alpha channels.
19 |
20 | This software uses zlib which is (C) Jean-loup Gailly and Mark Adler.
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from distutils.core import setup
3 |
4 | setup(
5 | name = 'psdparser',
6 | version = '0.1',
7 | author = 'Jeremy Bethmont',
8 | author_email = 'jeremy.bethmont@gmail.com',
9 | url = 'https://github.com/jerem/psdparse',
10 |
11 | description = 'PSD parser',
12 | long_description = open('README.txt').read(),
13 |
14 | license = 'GPLv2',
15 | packages = ['psdparse'],
16 | scripts=['bin/psdparser.py'],
17 | requires=['PyYAML', 'PIL'],
18 |
19 | classifiers=[
20 | 'Development Status :: 3 - Alpha',
21 | 'Intended Audience :: Developers',
22 | 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)',
23 | 'Programming Language :: Python',
24 | 'Programming Language :: Python :: 2',
25 | 'Programming Language :: Python :: 2.5',
26 | 'Programming Language :: Python :: 2.6',
27 | 'Programming Language :: Python :: 2.7',
28 | 'Topic :: Multimedia :: Graphics',
29 | 'Topic :: Multimedia :: Graphics :: Viewers',
30 | 'Topic :: Multimedia :: Graphics :: Graphics Conversion',
31 | 'Topic :: Software Development :: Libraries :: Python Modules',
32 | ],
33 | )
34 |
--------------------------------------------------------------------------------
/bin/psdparser.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | from __future__ import absolute_import
4 | import sys
5 | import logging
6 | from psdparse import psdparser
7 |
8 | if __name__ == '__main__':
9 | """
10 | Let's get started!
11 | """
12 |
13 | from optparse import OptionParser
14 | parser = OptionParser(usage = "usage: %prog [OPTS] PSDFILE.psd")
15 | po = parser.add_option
16 |
17 | # Verbosity
18 | po('-v', '--verbose', default=False, action='store_true')
19 | po('-q', '--quiet', default=False, action='store_true')
20 |
21 | (OPTS, args) = parser.parse_args()
22 |
23 | # The global holder for all the information we really care about
24 | info = {}
25 |
26 | # Set the logger
27 | level = logging.INFO
28 | if OPTS.verbose:
29 | level = logging.DEBUG
30 | if OPTS.quiet:
31 | level = logging.WARNING
32 |
33 | handler = logging.StreamHandler()
34 | handler.setFormatter(logging.Formatter('[DEBUG] %(message)s'))
35 | psdparser.logger.addHandler(handler)
36 | psdparser.logger.setLevel(level)
37 |
38 | # Run
39 | if len(args) == 1:
40 | psd = psdparser.PSDParser(args[0])
41 | psd.parse()
42 |
43 | import yaml
44 | print "# YAML automatically generated by psdparser\n"
45 | print yaml.dump({'header': psd.header, 'ressources': psd.ressources, 'layers': psd.layers}, indent=2)
46 |
47 | else:
48 | parser.print_help()
49 | sys.exit(1)
50 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 |
294 | Copyright (C)
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | , 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
--------------------------------------------------------------------------------
/psdparse/psdparser.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | # This file is part of psdparser.
4 | #
5 | # Psdparser is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # Psdparser is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with Psdparser. If not, see .
17 |
18 | import logging
19 | import sys
20 | from struct import unpack, calcsize
21 | from PIL import Image
22 |
23 | logger = logging.getLogger(__name__)
24 |
25 | """
26 | Header mode field meanings
27 | """
28 | CHANNEL_SUFFIXES = {
29 | -2: 'layer mask',
30 | -1: 'A',
31 | 0: 'R',
32 | 1: 'G',
33 | 2: 'B',
34 | 3: 'RGB',
35 | 4: 'CMYK', 5: 'HSL', 6: 'HSB',
36 | 9: 'Lab', 11: 'RGB',
37 | 12: 'Lab', 13: 'CMYK',
38 | }
39 |
40 | """
41 | Resource id descriptions
42 | """
43 | RESOURCE_DESCRIPTIONS = {
44 | 1000: 'PS2.0 mode data',
45 | 1001: 'Macintosh print record',
46 | 1003: 'PS2.0 indexed color table',
47 | 1005: 'ResolutionInfo',
48 | 1006: 'Names of the alpha channels',
49 | 1007: 'DisplayInfo',
50 | 1008: 'Caption',
51 | 1009: 'Border information',
52 | 1010: 'Background color',
53 | 1011: 'Print flags',
54 | 1012: 'Grayscale/multichannel halftoning info',
55 | 1013: 'Color halftoning info',
56 | 1014: 'Duotone halftoning info',
57 | 1015: 'Grayscale/multichannel transfer function',
58 | 1016: 'Color transfer functions',
59 | 1017: 'Duotone transfer functions',
60 | 1018: 'Duotone image info',
61 | 1019: 'B&W values for the dot range',
62 | 1021: 'EPS options',
63 | 1022: 'Quick Mask info',
64 | 1024: 'Layer state info',
65 | 1025: 'Working path',
66 | 1026: 'Layers group info',
67 | 1028: 'IPTC-NAA record (File Info)',
68 | 1029: 'Image mode for raw format files',
69 | 1030: 'JPEG quality',
70 | 1032: 'Grid and guides info',
71 | 1033: 'Thumbnail resource',
72 | 1034: 'Copyright flag',
73 | 1035: 'URL',
74 | 1036: 'Thumbnail resource',
75 | 1037: 'Global Angle',
76 | 1038: 'Color samplers resource',
77 | 1039: 'ICC Profile',
78 | 1040: 'Watermark',
79 | 1041: 'ICC Untagged',
80 | 1042: 'Effects visible',
81 | 1043: 'Spot Halftone',
82 | 1044: 'Document specific IDs',
83 | 1045: 'Unicode Alpha Names',
84 | 1046: 'Indexed Color Table Count',
85 | 1047: 'Transparent Index',
86 | 1049: 'Global Altitude',
87 | 1050: 'Slices',
88 | 1051: 'Workflow URL',
89 | 1052: 'Jump To XPEP',
90 | 1053: 'Alpha Identifiers',
91 | 1054: 'URL List',
92 | 1057: 'Version Info',
93 | 2999: 'Name of clipping path',
94 | 10000: 'Print flags info',
95 | }
96 |
97 | MODES = {
98 | 0: 'Bitmap',
99 | 1: 'GrayScale',
100 | 2: 'IndexedColor',
101 | 3: 'RGBColor',
102 | 4: 'CMYKColor',
103 | 5: 'HSLColor',
104 | 6: 'HSBColor',
105 | 7: 'Multichannel',
106 | 8: 'Duotone',
107 | 9: 'LabColor',
108 | 10: 'Gray16',
109 | 11: 'RGB48',
110 | 12: 'Lab48',
111 | 13: 'CMYK64',
112 | 14: 'DeepMultichannel',
113 | 15: 'Duotone16',
114 | }
115 |
116 | COMPRESSIONS = {
117 | 0: 'Raw',
118 | 1: 'RLE',
119 | 2: 'ZIP',
120 | 3: 'ZIPPrediction',
121 | }
122 |
123 | BLENDINGS = {
124 | 'norm': 'normal',
125 | 'dark': 'darken',
126 | 'mul ': 'multiply',
127 | 'lite': 'lighten',
128 | 'scrn': 'screen',
129 | 'over': 'overlay',
130 | 'sLit': 'soft-light',
131 | 'hLit': 'hard-light',
132 | 'lLit': 'linear-light',
133 | 'diff': 'difference',
134 | 'smud': 'exclusion',
135 | }
136 |
137 | PIL_BANDS = {
138 | 'R': 0,
139 | 'G': 1,
140 | 'B': 2,
141 | 'A': 3,
142 | 'L': 0,
143 | }
144 |
145 | def INDENT_OUTPUT(depth, msg):
146 | return ''.join([' ' for i in range(0, depth)]) + msg
147 |
148 | class PSDParser(object):
149 |
150 | header = None
151 | ressources = None
152 | num_layers = 0
153 | layers = None
154 | images = None
155 | merged_image = None
156 |
157 | def __init__(self, filename):
158 | self.filename = filename
159 |
160 | def _pad2(self, i):
161 | """same or next even"""
162 | return (i + 1) / 2 * 2
163 |
164 | def _pad4(self, i):
165 | """same or next multiple of 4"""
166 | return (i + 3) / 4 * 4
167 |
168 | def _readf(self, format):
169 | """read a strct from file structure according to format"""
170 | return unpack(format, self.fd.read(calcsize(format)))
171 |
172 | def _skip_block(self, desc, indent=0, new_line=False):
173 | (n,) = self._readf('>L') # (n,) is a 1-tuple.
174 | if n:
175 | self.fd.seek(n, 1) # 1: relative
176 |
177 | if new_line:
178 | logger.debug('')
179 | logger.debug(INDENT_OUTPUT(indent, 'Skipped %s with %s bytes' % (desc, n)))
180 |
181 | def parse(self):
182 | logger.debug("Opening '%s'" % self.filename)
183 |
184 | self.fd = open(self.filename, 'rb')
185 | try:
186 | self.parse_header()
187 | self.parse_image_resources()
188 | self.parse_layers_and_masks()
189 | self.parse_image_data()
190 | finally:
191 | self.fd.close()
192 |
193 | logger.debug("")
194 | logger.debug("DONE")
195 |
196 | def parse_header(self):
197 | logger.debug("")
198 | logger.debug("# Header #")
199 |
200 | self.header = {}
201 |
202 | C_PSD_HEADER = ">4sH 6B HLLHH"
203 | (
204 | self.header['sig'],
205 | self.header['version'],
206 | self.header['r0'],
207 | self.header['r1'],
208 | self.header['r2'],
209 | self.header['r3'],
210 | self.header['r4'],
211 | self.header['r5'],
212 | self.header['channels'],
213 | self.header['rows'],
214 | self.header['cols'],
215 | self.header['depth'],
216 | self.header['mode']
217 | ) = self._readf(C_PSD_HEADER)
218 |
219 | self.size = [self.header['rows'], self.header['cols']]
220 |
221 | if self.header['sig'] != "8BPS":
222 | raise ValueError("Not a PSD signature: '%s'" % self.header['sig'])
223 | if self.header['version'] != 1:
224 | raise ValueError("Can not handle PSD version:%d" % self.header['version'])
225 | self.header['modename'] = MODES[self.header['mode']] if 0 <= self.header['mode'] < 16 else "(%s)" % self.header['mode']
226 |
227 | logger.debug(INDENT_OUTPUT(1, "channels:%(channels)d, rows:%(rows)d, cols:%(cols)d, depth:%(depth)d, mode:%(mode)d [%(modename)s]" % self.header))
228 | logger.debug(INDENT_OUTPUT(1, "%s" % self.header))
229 |
230 | # Remember position
231 | self.header['colormodepos'] = self.fd.tell()
232 | self._skip_block("color mode data", 1)
233 |
234 | def parse_image_resources(self):
235 | def parse_irb():
236 | """return total bytes in block"""
237 | r = {}
238 | r['at'] = self.fd.tell()
239 | (r['type'], r['id'], r['namelen']) = self._readf(">4s H B")
240 | n = self._pad2(r['namelen'] + 1) - 1
241 | (r['name'],) = self._readf(">%ds" % n)
242 | r['name'] = r['name'][:-1] # skim off trailing 0byte
243 | r['short'] = r['name'][:20]
244 | (r['size'],) = self._readf(">L")
245 | self.fd.seek(self._pad2(r['size']), 1) # 1: relative
246 | r['rdesc'] = "[%s]" % RESOURCE_DESCRIPTIONS.get(r['id'], "?")
247 | logger.debug(INDENT_OUTPUT(1, "Resource: %s" % r))
248 | logger.debug(INDENT_OUTPUT(1, "0x%(at)06x| type:%(type)s, id:%(id)5d, size:0x%(size)04x %(rdesc)s '%(short)s'" % r))
249 | self.ressources.append(r)
250 | return 4 + 2 + self._pad2(1 + r['namelen']) + 4 + self._pad2(r['size'])
251 |
252 | logger.debug("")
253 | logger.debug("# Ressources #")
254 | self.ressources = []
255 | (n,) = self._readf(">L") # (n,) is a 1-tuple.
256 | while n > 0:
257 | n -= parse_irb()
258 | if n != 0:
259 | logger.debug("Image resources overran expected size by %d bytes" % (-n))
260 |
261 | def parse_image(self, li, is_layer=True):
262 | def parse_channel(li, idx, count, rows, cols, depth):
263 | """params:
264 | li -- layer info struct
265 | idx -- channel number
266 | count -- number of channels to process ???
267 | rows, cols -- dimensions
268 | depth -- bits
269 | """
270 | chlen = li['chlengths'][idx]
271 | if chlen is not None and chlen < 2:
272 | raise ValueError("Not enough channel data: %s" % chlen)
273 | if li['chids'][idx] == -2:
274 | rows, cols = li['mask']['rows'], li['mask']['cols']
275 |
276 | rb = (cols * depth + 7) / 8 # round to next byte
277 |
278 | # channel header
279 | chpos = self.fd.tell()
280 | (comp,) = self._readf(">H")
281 |
282 | if chlen:
283 | chlen -= 2
284 |
285 | pos = self.fd.tell()
286 |
287 | # If empty layer
288 | if cols * rows == 0:
289 | logger.debug(INDENT_OUTPUT(1, "Empty channel, skiping"))
290 | return
291 |
292 | if COMPRESSIONS.get(comp) == 'RLE':
293 | logger.debug(INDENT_OUTPUT(1, "Handling RLE compressed data"))
294 | rlecounts = 2 * count * rows
295 | if chlen and chlen < rlecounts:
296 | raise ValueError("Channel too short for RLE row counts (need %d bytes, have %d bytes)" % (rlecounts,chlen))
297 | pos += rlecounts # image data starts after RLE counts
298 | rlecounts_data = self._readf(">%dH" % (count * rows))
299 | for ch in range(count):
300 | rlelen_for_channel = sum(rlecounts_data[ch * rows:(ch + 1) * rows])
301 | data = self.fd.read(rlelen_for_channel)
302 | channel_name = CHANNEL_SUFFIXES[li['chids'][idx]]
303 | if li['channels'] == 2 and channel_name == 'B': channel_name = 'L'
304 | p = Image.fromstring("L", (cols, rows), data, "packbits", "L" )
305 | if is_layer:
306 | if channel_name in PIL_BANDS:
307 | self.images[li['idx']][PIL_BANDS[channel_name]] = p
308 | else:
309 | self.merged_image.append(p)
310 |
311 | elif COMPRESSIONS.get(comp) == 'Raw':
312 | logger.debug(INDENT_OUTPUT(1, "Handling Raw compressed data"))
313 |
314 | for ch in range(count):
315 | data = self.fd.read(cols * rows)
316 | channel_name = CHANNEL_SUFFIXES[li['chids'][idx]]
317 | if li['channels'] == 2 and channel_name == 'B': channel_name = 'L'
318 | p = Image.fromstring("L", (cols, rows), data, "raw", "L")
319 | if is_layer:
320 | if channel_name in PIL_BANDS:
321 | self.images[li['idx']][PIL_BANDS[channel_name]] = p
322 | else:
323 | self.merged_image.append(p)
324 |
325 | else:
326 | # TODO: maybe just skip channel...:
327 | # f.seek(chlen, SEEK_CUR)
328 | # return
329 | raise ValueError("Unsupported compression type: %s" % COMPRESSIONS.get(comp, comp))
330 |
331 | if (chlen is not None) and (self.fd.tell() != chpos + 2 + chlen):
332 | logger.debug("currentpos:%d should be:%d!" % (f.tell(), chpos + 2 + chlen))
333 | self.fd.seek(chpos + 2 + chlen, 0) # 0: SEEK_SET
334 |
335 | return
336 |
337 | if not self.header:
338 | self.parse_header()
339 | if not self.ressources:
340 | self._skip_block("image resources", new_line=True)
341 | self.ressources = 'not parsed'
342 |
343 | logger.debug("")
344 | logger.debug("# Image: %s/%d #" % (li['name'], li['channels']))
345 |
346 | # channels
347 | if is_layer:
348 | for ch in range(li['channels']):
349 | parse_channel(li, ch, 1, li['rows'], li['cols'], self.header['depth'])
350 | else:
351 | parse_channel(li, 0, li['channels'], li['rows'], li['cols'], self.header['depth'])
352 | return
353 |
354 | def _read_descriptor(self):
355 | # Descriptor
356 | def _unicode_string():
357 | len = self._readf(">L")[0]
358 | result = u''
359 | for count in range(len):
360 | val = self._readf(">H")[0]
361 | if val:
362 | result += unichr(val)
363 | return result
364 |
365 | def _string_or_key():
366 | len = self._readf(">L")[0]
367 | if not len:
368 | len = 4
369 | return self._readf(">%ds" % len)[0]
370 |
371 | def _desc_TEXT():
372 | return _unicode_string()
373 |
374 | def _desc_enum():
375 | return { 'typeID' : _string_or_key(),
376 | 'enum' : _string_or_key(),
377 | }
378 |
379 | def _desc_long():
380 | return self._readf(">l")[0]
381 |
382 | def _desc_bool():
383 | return self._readf(">?")[0]
384 |
385 | def _desc_doub():
386 | return self._readf(">d")[0]
387 |
388 | def _desc_tdta():
389 | # Apparently it is pdf data?
390 | # http://telegraphics.com.au/svn/psdparse
391 | # descriptor.c pdf.c
392 |
393 | len = self._readf(">L")[0]
394 | pdf_data = self.fd.read(len)
395 | return pdf_data
396 |
397 | _desc_item_factory = {
398 | 'TEXT' : _desc_TEXT,
399 | 'enum' : _desc_enum,
400 | 'long' : _desc_long,
401 | 'bool' : _desc_bool,
402 | 'doub' : _desc_doub,
403 | 'tdta' : _desc_tdta,
404 | }
405 |
406 | class_id_name = _unicode_string()
407 | class_id = _string_or_key()
408 | logger.debug(INDENT_OUTPUT(4, u"name='%s' clsid='%s'" % (class_id_name, class_id)))
409 |
410 | item_count = self._readf(">L")[0]
411 | #logger.debug(INDENT_OUTPUT(4, "item_count=%d" % (item_count)))
412 | items = {}
413 | for item_index in range(item_count):
414 | item_key = _string_or_key()
415 | item_type = self._readf(">4s")[0]
416 | if not item_type in _desc_item_factory:
417 | logger.debug(INDENT_OUTPUT(4, "unknown descriptor item '%s', skipping ahead." % item_type))
418 | break
419 |
420 | items[item_key] = _desc_item_factory[item_type]()
421 | #logger.debug(INDENT_OUTPUT(4, "item['%s']='%r'" % (item_key,items[item_key])))
422 | #print items[item_key]
423 | return items
424 |
425 | def parse_layers_and_masks(self):
426 |
427 | if not self.header:
428 | self.parse_header()
429 | if not self.ressources:
430 | self._skip_block('image resources', new_line=True)
431 | self.ressources = 'not parsed'
432 |
433 | logger.debug("")
434 | logger.debug("# Layers & Masks #")
435 |
436 | self.layers = []
437 | self.images = []
438 | self.header['mergedalpha'] = False
439 | (misclen,) = self._readf(">L")
440 | if misclen:
441 | miscstart = self.fd.tell()
442 | # process layer info section
443 | (layerlen,) = self._readf(">L")
444 | if layerlen:
445 | # layers structure
446 | (self.num_layers,) = self._readf(">h")
447 | if self.num_layers < 0:
448 | self.num_layers = -self.num_layers
449 | logger.debug(INDENT_OUTPUT(1, "First alpha is transparency for merged image"))
450 | self.header['mergedalpha'] = True
451 | logger.debug(INDENT_OUTPUT(1, "Layer info for %d layers:" % self.num_layers))
452 |
453 | if self.num_layers * (18 + 6 * self.header['channels']) > layerlen:
454 | raise ValueError("Unlikely number of %s layers for %s channels with %s layerlen. Giving up." % (self.num_layers, self.header['channels'], layerlen))
455 |
456 | linfo = [] # collect header infos here
457 |
458 | for i in range(self.num_layers):
459 | l = {}
460 | l['idx'] = i
461 |
462 | #
463 | # Layer Info
464 | #
465 | (l['top'], l['left'], l['bottom'], l['right'], l['channels']) = self._readf(">llllH")
466 | (l['rows'], l['cols']) = (l['bottom'] - l['top'], l['right'] - l['left'])
467 | logger.debug(INDENT_OUTPUT(1, "layer %(idx)d: (%(left)4d,%(top)4d,%(right)4d,%(bottom)4d), %(channels)d channels (%(cols)4d cols x %(rows)4d rows)" % l))
468 |
469 | # Sanity check
470 | if l['bottom'] < l['top'] or l['right'] < l['left'] or l['channels'] > 64:
471 | logger.debug(INDENT_OUTPUT(2, "Something's not right about that, trying to skip layer."))
472 | self.fd.seek(6 * l['channels'] + 12, 1) # 1: SEEK_CUR
473 | self._skip_block("layer info: extra data", 2)
474 | continue # next layer
475 |
476 | # Read channel infos
477 | l['chlengths'] = []
478 | l['chids'] = []
479 | # - 'hackish': addressing with -1 and -2 will wrap around to the two extra channels
480 | l['chindex'] = [ -1 ] * (l['channels'] + 2)
481 | for j in range(l['channels']):
482 | chid, chlen = self._readf(">hL")
483 | l['chids'].append(chid)
484 | l['chlengths'].append(chlen)
485 | logger.debug(INDENT_OUTPUT(3, "Channel %2d: id=%2d, %5d bytes" % (j, chid, chlen)))
486 | if -2 <= chid < l['channels']:
487 | # pythons negative list-indexs: [ 0, 1, 2, 3, ... -2, -1]
488 | l['chindex'][chid] = j
489 | else:
490 | logger.debug(INDENT_OUTPUT(3, "Unexpected channel id %d" % chid))
491 | l['chidstr'] = CHANNEL_SUFFIXES.get(chid, "?")
492 |
493 | # put channel info into connection
494 | linfo.append(l)
495 |
496 | #
497 | # Blend mode
498 | #
499 | bm = {}
500 |
501 | (bm['sig'], bm['key'], bm['opacity'], bm['clipping'], bm['flags'], bm['filler'],
502 | ) = self._readf(">4s4sBBBB")
503 | bm['opacp'] = (bm['opacity'] * 100 + 127) / 255
504 | bm['clipname'] = bm['clipping'] and "non-base" or "base"
505 | bm['blending'] = BLENDINGS.get(bm['key'])
506 | l['blend_mode'] = bm
507 |
508 | logger.debug(INDENT_OUTPUT(3, "Blending mode: sig=%(sig)s key=%(key)s opacity=%(opacity)d(%(opacp)d%%) clipping=%(clipping)d(%(clipname)s) flags=%(flags)x" % bm))
509 |
510 | # remember position for skipping unrecognized data
511 | (extralen,) = self._readf(">L")
512 | extrastart = self.fd.tell()
513 |
514 | #
515 | # Layer mask data
516 | #
517 | m = {}
518 | (m['size'],) = self._readf(">L")
519 | if m['size']:
520 | (m['top'], m['left'], m['bottom'], m['right'], m['default_color'], m['flags'],
521 | ) = self._readf(">llllBB")
522 | # skip remainder
523 | self.fd.seek(m['size'] - 18, 1) # 1: SEEK_CUR
524 | m['rows'], m['cols'] = m['bottom'] - m['top'], m['right'] - m['left']
525 | l['mask'] = m
526 |
527 | self._skip_block("layer blending ranges", 3)
528 |
529 | #
530 | # Layer name
531 | #
532 | name_start = self.fd.tell()
533 | (l['namelen'],) = self._readf(">B")
534 | addl_layer_data_start = name_start + self._pad4(l['namelen'] + 1)
535 | # - "-1": one byte traling 0byte. "-1": one byte garble.
536 | # (l['name'],) = readf(f, ">%ds" % (self._pad4(1+l['namelen'])-2))
537 | (l['name'],) = self._readf(">%ds" % (l['namelen']))
538 |
539 | logger.debug(INDENT_OUTPUT(3, "Name: '%s'" % l['name']))
540 |
541 | self.fd.seek(addl_layer_data_start, 0)
542 |
543 |
544 | #
545 | # Read add'l Layer Information
546 | #
547 | while self.fd.tell() < extrastart + extralen:
548 | (signature, key, size, ) = self._readf(">4s4sL") # (n,) is a 1-tuple.
549 | logger.debug(INDENT_OUTPUT(3, "Addl info: sig='%s' key='%s' size='%d'" %
550 | (signature, key, size)))
551 | next_addl_offset = self.fd.tell() + self._pad2(size)
552 |
553 | if key == 'luni':
554 | namelen = self._readf(">L")[0]
555 | l['name'] = u''
556 | for count in range(0, namelen):
557 | l['name'] += unichr(self._readf(">H")[0])
558 |
559 | logger.debug(INDENT_OUTPUT(4, u"Unicode Name: '%s'" % l['name']))
560 | elif key == 'TySh':
561 | version = self._readf(">H")[0]
562 | (xx, xy, yx, yy, tx, ty,) = self._readf(">dddddd") #transform
563 | text_version = self._readf(">H")[0]
564 | text_desc_version = self._readf(">L")[0]
565 | text_desc = self._read_descriptor()
566 | warp_version = self._readf(">H")[0]
567 | warp_desc_version = self._readf(">L")[0]
568 | warp_desc = self._read_descriptor()
569 | (left,top,right,bottom,) = self._readf(">llll")
570 |
571 | logger.debug(INDENT_OUTPUT(4, "ver=%d tver=%d dver=%d"
572 | % (version, text_version, text_desc_version)))
573 | logger.debug(INDENT_OUTPUT(4, "%f %f %f %f %f %f" % (xx, xy, yx, yy, tx, ty,)))
574 | logger.debug(INDENT_OUTPUT(4, "l=%f t=%f r=%f b=%f"
575 | % (left, top, right, bottom)))
576 |
577 | l['text_layer'] = {}
578 | l['text_layer']['text_desc'] = text_desc
579 | l['text_layer']['text_transform'] = (xx, xy, yx, yy, tx, ty,)
580 | l['text_layer']['left'] = left
581 | l['text_layer']['top'] = top
582 | l['text_layer']['right'] = right
583 | l['text_layer']['bottom'] = bottom
584 |
585 | self.fd.seek(next_addl_offset, 0)
586 |
587 | # Skip over any extra data
588 | self.fd.seek(extrastart + extralen, 0) # 0: SEEK_SET
589 |
590 | self.layers.append(l)
591 |
592 | for i in range(self.num_layers):
593 | # Empty layer
594 | if linfo[i]['rows'] * linfo[i]['cols'] == 0:
595 | self.images.append(None)
596 | self.parse_image(linfo[i], is_layer=True)
597 | continue
598 |
599 | self.images.append([0, 0, 0, 0])
600 | self.parse_image(linfo[i], is_layer=True)
601 | if linfo[i]['channels'] == 2:
602 | l = self.images[i][0]
603 | a = self.images[i][3]
604 | self.images[i] = Image.merge('LA', [l, a])
605 | else:
606 | # is there an alpha channel?
607 | if type(self.images[i][3]) is int:
608 | self.images[i] = Image.merge('RGB', self.images[i][0:3])
609 | else:
610 | self.images[i] = Image.merge('RGBA', self.images[i])
611 |
612 | else:
613 | logger.debug(INDENT_OUTPUT(1, "Layer info section is empty"))
614 |
615 | skip = miscstart + misclen - self.fd.tell()
616 | if skip:
617 | logger.debug("")
618 | logger.debug("Skipped %d bytes at end of misc data?" % skip)
619 | self.fd.seek(skip, 1) # 1: SEEK_CUR
620 | else:
621 | logger.debug(INDENT_OUTPUT(1, "Misc info section is empty"))
622 |
623 | def parse_image_data(self):
624 |
625 | if not self.header:
626 | self.parse_header()
627 | if not self.ressources:
628 | self._skip_block("image resources", new_line=True)
629 | self.ressources = 'not parsed'
630 | if not self.layers:
631 | self._skip_block("image layers", new_line=True)
632 | self.layers = 'not parsed'
633 |
634 | self.merged_image = []
635 | li = {}
636 | li['chids'] = range(self.header['channels'])
637 | li['chlengths'] = [ None ] * self.header['channels'] # dummy data
638 | (li['name'], li['channels'], li['rows'], li['cols']) = ('merged', self.header['channels'], self.header['rows'], self.header['cols'])
639 | li['layernum'] = -1
640 | self.parse_image(li, is_layer=False)
641 | if li['channels'] == 1:
642 | self.merged_image = self.merged_image[0]
643 | elif li['channels'] == 3:
644 | self.merged_image = Image.merge('RGB', self.merged_image)
645 | elif li['channels'] >= 4 and self.header['mode'] == 3:
646 | self.merged_image = Image.merge('RGBA', self.merged_image[:4])
647 | else:
648 | raise ValueError('Unsupported mode or number of channels')
649 |
650 |
--------------------------------------------------------------------------------