postr-0.12.4/0000755000175000017500000000000011274426371011446 5ustar gpoogpoopostr-0.12.4/postr0000755000175000017500000000247211274363367012555 0ustar gpoogpoo#! /usr/bin/env python # Postr, a Flickr Uploader # # Copyright (C) 2006-2007 Ross Burton # # 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 2, 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, write to the Free Software Foundation, Inc., 51 Franklin # St, Fifth Floor, Boston, MA 02110-1301 USA import gettext gettext.install('postr') import sys from twisted.internet import gtk2reactor reactor = gtk2reactor.install() # Import from src first so that we can run directly from the source tree for # development. try: from src import postr except ImportError, e: from postr import postr p = postr.Postr() if p.is_running(): for url in sys.argv[1:]: p.open_uri(url) sys.exit(0) else: p.window.show() p.add_window(p.window) for url in sys.argv[1:]: p.add_image_filename(url) reactor.run() postr-0.12.4/COPYING0000644000175000017500000004311011274363367012505 0ustar gpoogpoo GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) 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 this service 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 make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. 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. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the 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 a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE 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. 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 convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This 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 2 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision 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, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This 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 Library General Public License instead of this License. postr-0.12.4/data/0000755000175000017500000000000011274424736012362 5ustar gpoogpoopostr-0.12.4/data/postr.desktop0000644000175000017500000000031511274424736015123 0ustar gpoogpoo[Desktop Entry] Encoding=UTF-8 Name=Flickr Uploader Comment=Upload photos to Flickr Icon=postr TryExec=postr Exec=postr %F StartupNotify=true Terminal=false Type=Application Categories=GNOME;GTK;Graphics; postr-0.12.4/data/24x24/0000755000175000017500000000000011274363367013147 5ustar gpoogpoopostr-0.12.4/data/24x24/postr.png0000644000175000017500000000242311274363367015025 0ustar gpoogpooPNG  IHDRw=bKGD pHYs B(xtIME2­IDATHǽmLUu?r.\{B@ |6CD_8JoPWZ^\75_ù2Rz=T4uT^UGyo>Y>yDGkɭ0 ףqdeYV@=D\,lu)M$1usJ`'kx<-@sc "ܐچAAσk'j2b3 \r过-Tt)#6KQQ;]wRZHOOIdFS9Ogpe2d" F¹wvp-ٱ=?v*t]O~XTU53o|,LhC9ю\s Ce2QUF|.@SSSFNm[ii3ڐ ;$f:85S+r<_jJ(̨K ! m bpIENDB`postr-0.12.4/data/32x32/0000755000175000017500000000000011274363367013145 5ustar gpoogpoopostr-0.12.4/data/32x32/postr.svg0000644000175000017500000004443511274363367015047 0ustar gpoogpoo image/svg+xml Postr application icon April 2007 Andreas Nilsson OpenedHand Ltd. http://www.o-hand.com postr-0.12.4/data/32x32/postr.png0000644000175000017500000000460211274363367015024 0ustar gpoogpooPNG  IHDR szzsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<IDATXilszwجp&\6RMH6jFR"UJjB./-rpJm>D =BsTm.(2M0G8R0z;8-#hv<3/M Zaf' 6*@9PTȲ\)ryjmYmv# [ UU狢Xn(3f0KJJbm|%FN(iN֟06y``f&Ӷvg +cB!VVٙ&zw4nAW#-w{χFF񣏅um]u`槒⟒۷o7t;b, tGgѢAHvEv1\L4'9u0.ӿv*L mXˁOk\QLa a20j]`=z=0 PT$yaQ:LY 0ԑ_l 5'|܅bQ^V䓦"@˲р*؜(mJB%K "L:4ԐF y5T G͊>ɾ+rN -[_8V-dx4QPy[ L\?+lxrxMf/ YsbmZ_NrS{{3ZDQ3 h$?.윫u}@IȺxǡ;MZ>Q\Xɂd`8`9I5s"K 褙uݿQŚoU/`j @ e10va5D,#RE?xrWqV&jNpŦ׸\EG?0 \C-aR.0/,ι^p_"i 1xc ƽjnn}ߙ Ё>>| hXrYɓV:|D<˲KN{ss;s `qᡇ/,\E*.X,Z(~Oׯ|OM  uuuZu]P NUL/k>cb n*AޖIENDB`postr-0.12.4/data/16x16/0000755000175000017500000000000011274363367013151 5ustar gpoogpoopostr-0.12.4/data/16x16/postr.svg0000644000175000017500000001621411274363367015045 0ustar gpoogpoo image/svg+xml postr-0.12.4/data/16x16/postr.png0000644000175000017500000000107011274363367015024 0ustar gpoogpooPNG  IHDRasBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<IDAT8MKQZRBEi€cJuP.*pҍP 5B\ qE.,(QtLL1qW(~.23dBY.ϿBĤ*v'$S:v^K@H) #y u:v}RӔ;~?$CmS[϶zH.EsAS:aqIENDB`postr-0.12.4/data/22x22/0000755000175000017500000000000011274363367013143 5ustar gpoogpoopostr-0.12.4/data/22x22/postr.svg0000644000175000017500000002335611274363367015044 0ustar gpoogpoo image/svg+xml postr-0.12.4/data/22x22/postr.png0000644000175000017500000000244211274363367015022 0ustar gpoogpooPNG  IHDRĴl;sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<IDAT8{Le?9?Ώ" ‚@R++mFxil @]77ZSsAene" A?{?"e$PX[#@i$oQ\qJK^ɈGUگbZN݋^@9G8/ ^*|"5_F8r>X,+u 4[A w!Iyp|Ӯi*=aּ&M=#`v7xjR05c4 ,$<#>lTѱA"\[(J`a܇֣mGl;bJBp "NVj image/svg+xml Postr application icon April 2007 Andreas Nilsson OpenedHand Ltd. http://www.o-hand.com postr-0.12.4/src/0000755000175000017500000000000011274426426012236 5ustar gpoogpoopostr-0.12.4/src/SafetyCombo.py0000644000175000017500000000305411274363367015031 0ustar gpoogpoo# Postr, a Flickr Uploader # # Copyright (C) 2006-2008 Ross Burton # # 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 2, 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, write to the Free Software Foundation, Inc., 51 Franklin # St, Fifth Floor, Boston, MA 02110-1301 USA import gobject, gtk class SafetyCombo(gtk.ComboBox): def __init__(self): gtk.ComboBox.__init__(self) # Name, is_public, is_family, is_friend model = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_INT) model.set(model.append(), 0, "Safe", 1, 1) model.set(model.append(), 0, "Moderate", 1, 2) model.set(model.append(), 0, "Restricted", 1, 3) self.model = model self.set_model(model) self.set_active(0) cell = gtk.CellRendererText() self.pack_start(cell) self.add_attribute(cell, "text", 0) def get_safety_for_iter(self, it): if it is None: return None return self.model.get_value(it, 1) def get_active_safety(self): return self.get_safety_for_iter(self.get_active_iter()) postr-0.12.4/src/StatusBar.py0000644000175000017500000000470011274424736014523 0ustar gpoogpoo# Postr, a Flickr Uploader # # Copyright (C) 2006-2008 Ross Burton # # 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 2, 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, write to the Free Software Foundation, Inc., 51 Franklin # St, Fifth Floor, Boston, MA 02110-1301 USA import gtk from ErrorDialog import ErrorDialog from util import greek class StatusBar(gtk.Statusbar): def __init__(self, flickr): gtk.Statusbar.__init__(self) self.context = self.get_context_id("quota") self.flickr = flickr self.quota = None self.to_upload = None def __update(self): self.pop(self.context) if self.quota and self.to_upload: message = _("You have %(quota)s remaining this month (%(to_upload)s to upload)") % self.__dict__ elif self.quota: message = _("You have %(quota)s remaining this month") % self.__dict__ elif self.to_upload: message = _("%(to_upload)s to upload") % self.__dict__ else: message = "" if self.flickr.get_username(): name = self.flickr.get_fullname() or self.flickr.get_username() message = message + " - logged in as " + name self.push(self.context, message) def update_quota(self): """Call Flickr to get the current upload quota, and update the status bar.""" def got_quota(rsp): self.quota = greek(int(rsp.find("user/bandwidth").get("remainingbytes"))) self.__update() def error(failure): dialog = ErrorDialog(self.get_toplevel()) dialog.set_from_failure(failure) dialog.show_all() self.flickr.people_getUploadStatus().addCallbacks(got_quota, error) def set_upload(self, to_upload): """Set the amount of data to be uploaded, and update the status bar.""" if to_upload: self.to_upload = greek(to_upload) else: self.to_upload = None self.__update() postr-0.12.4/src/ErrorDialog.py0000644000175000017500000000313311274424736015023 0ustar gpoogpoo# Postr, a Flickr Uploader # # Copyright (C) 2006-2008 Ross Burton # # 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 2, 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, write to the Free Software Foundation, Inc., 51 Franklin # St, Fifth Floor, Boston, MA 02110-1301 USA import gtk class ErrorDialog(gtk.MessageDialog): def __init__(self, parent=None): gtk.MessageDialog.__init__(self, flags=gtk.DIALOG_DESTROY_WITH_PARENT, type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_OK, parent=parent, message_format="An error occurred") self.connect("response", lambda dialog, response: dialog.destroy()) def set_from_failure (self, failure): # TODO: format nicer self.format_secondary_text (str (failure.value)) def set_from_exception (self, exception): # TODO: format nicer self.format_secondary_text (str (exception)) def set_from_string(self, message): # TODO: format nicer self.format_secondary_text (message) postr-0.12.4/src/ProgressDialog.py0000644000175000017500000000406411274363367015544 0ustar gpoogpoo# Postr, a Flickr Uploader # # Copyright (C) 2006-2008 Ross Burton # # 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 2, 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, write to the Free Software Foundation, Inc., 51 Franklin # St, Fifth Floor, Boston, MA 02110-1301 USA import gtk class ProgressDialog(gtk.Dialog): def __init__(self, cancel_cb): gtk.Dialog.__init__(self, title="", flags=gtk.DIALOG_NO_SEPARATOR) self.cancel_cb = cancel_cb self.set_resizable(False) self.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) self.connect("response", self.on_response) vbox = gtk.VBox(False, 8) vbox.set_border_width(8) self.vbox.add(vbox) hbox = gtk.HBox(False, 8) vbox.add (hbox) self.thumbnail = gtk.Image() hbox.pack_start (self.thumbnail, False, False, 0) self.label = gtk.Label() self.label.set_alignment (0.0, 0.0) hbox.pack_start (self.label, True, True, 0) self.progress = gtk.ProgressBar() vbox.add(self.progress) vbox.show_all() def on_response(self, dialog, response): if response == gtk.RESPONSE_CANCEL or response == gtk.RESPONSE_DELETE_EVENT: self.cancel_cb() if __name__ == "__main__": import gobject d = ProgressDialog() d.thumbnail.set_from_icon_name ("stock_internet", gtk.ICON_SIZE_DIALOG) d.label.set_text("Uploading") def pulse(): d.progress.pulse() return True gobject.timeout_add(200, pulse) d.show() gtk.main() postr-0.12.4/src/PrivacyCombo.py0000644000175000017500000000351311274363367015213 0ustar gpoogpoo# Postr, a Flickr Uploader # # Copyright (C) 2006-2008 Ross Burton # # 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 2, 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, write to the Free Software Foundation, Inc., 51 Franklin # St, Fifth Floor, Boston, MA 02110-1301 USA import gobject, gtk class PrivacyCombo(gtk.ComboBox): def __init__(self): gtk.ComboBox.__init__(self) # Name, is_public, is_family, is_friend model = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_BOOLEAN, gobject.TYPE_BOOLEAN, gobject.TYPE_BOOLEAN) model.set(model.append(), 0, "Public", 1, True, 2, False, 3, False) model.set(model.append(), 0, "Family Only", 1, False, 2, True, 3, False) model.set(model.append(), 0, "Friends and Family Only", 1, False, 2, True, 3, True) model.set(model.append(), 0, "Private", 1, False, 2, False, 3, False) self.model = model self.set_model(model) self.set_active(0) cell = gtk.CellRendererText() self.pack_start(cell) self.add_attribute(cell, "text", 0) # (is_public, is_family, is_friend) def get_active_acls(self): return self.get_acls_for_iter(self.get_active_iter()) # (is_public, is_family, is_friend) def get_acls_for_iter(self, it): if it is None: return None return self.model.get(it, 1, 2, 3) postr-0.12.4/src/AboutDialog.py0000644000175000017500000000247711274424736015016 0ustar gpoogpoo# Postr, a Flickr Uploader # # Copyright (C) 2006-2008 Ross Burton # # 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 2, 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, write to the Free Software Foundation, Inc., 51 Franklin # St, Fifth Floor, Boston, MA 02110-1301 USA import gtk from version import __version__ class AboutDialog(gtk.AboutDialog): def __init__(self): gtk.AboutDialog.__init__(self) self.set_name(_('Flickr Uploader')) self.set_copyright(u'Copyright \u00A9 2006-2008 Ross Burton') self.set_authors(('Ross Burton ',)) self.set_website('http://burtonini.com/') self.set_logo_icon_name('postr') self.set_version (__version__) if __name__ == "__main__": import gettext; gettext.install('postr') AboutDialog().show() gtk.main() postr-0.12.4/src/version.py0000644000175000017500000000002711274426056014273 0ustar gpoogpoo__version__ = '0.12.4' postr-0.12.4/src/iptcinfo.py0000644000175000017500000012211011274363367014424 0ustar gpoogpoo# :mode=python:encoding=ISO-8859-2: # -*- coding: iso-8859-2 -*- # Author: 2004 Gulcsi Tams # # Ported from Josh Carter's Perl IPTCInfo.pm by Tam?s Gul?csi # # IPTCInfo: extractor for IPTC metadata embedded in images # Copyright (C) 2000-2004 Josh Carter # All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the same terms as Python itself. # # VERSION = '1.9'; """ IPTCInfo - Python module for extracting and modifying IPTC image meta-data Ported from Josh Carter's Perl IPTCInfo-1.9.pm by Tams Gulcsi Ever wish you add information to your photos like a caption, the place you took it, the date, and perhaps even keywords and categories? You already can. The International Press Telecommunications Council (IPTC) defines a format for exchanging meta-information in news content, and that includes photographs. You can embed all kinds of information in your images. The trick is putting it to use. That's where this IPTCInfo Python module comes into play. You can embed information using many programs, including Adobe Photoshop, and IPTCInfo will let your web server -- and other automated server programs -- pull it back out. You can use the information directly in Python programs, export it to XML, or even export SQL statements ready to be fed into a database. PREFACE First, I want to apologize a little bit: as this module is originally written in Perl by Josh Carter, it is quite non-Pythonic (for example the addKeyword, clearSupplementalCategories functions - I think it would be better having a derived list class with add, clear functions) and tested only by me reading/writing IPTC metadata for family photos. Any suggestions welcomed! Thanks, Tams Gulcsi SYNOPSIS from iptcinfo import IPTCInfo import sys fn = (len(sys.argv) > 1 and [sys.argv[1]] or ['test.jpg'])[0] fn2 = (len(sys.argv) > 2 and [sys.argv[2]] or ['test_out.jpg'])[0] # Create new info object info = IPTCInfo(fn) # Check if file had IPTC data if len(info.data) < 4: raise Exception(info.error) # Print list of keywords, supplemental categories, or contacts print info.keywords print info.supplementalCategories print info.contacts # Get specific attributes... caption = info.data['caption/abstract'] # Create object for file that may or may not have IPTC data. info = IPTCInfo(fn) # Add/change an attribute info.data['caption/abstract'] = 'Witty caption here' info.data['supplemental category'] = ['portrait'] # Save new info to file ##### See disclaimer in 'SAVING FILES' section ##### info.save() info.saveAs(fn2) #re-read IPTC info print IPTCInfo(fn2) DESCRIPTION USING IPTCINFO To integrate with your own code, simply do something like what's in the synopsys above. The complete list of possible attributes is given below. These are as specified in the IPTC IIM standard, version 4. Keywords and categories are handled slightly differently: since these are lists, the module allows you to access them as Python lists. Call keywords() and supplementalCategories() to get each list. IMAGES NOT HAVING IPTC METADATA If yout apply info = IPTCInfo('file-name-here.jpg') to an image not having IPTC metadata, len(info.data) will be 3 ('supplemental categories', 'keywords', 'contacts') will be empty lists. MODIFYING IPTC DATA You can modify IPTC data in JPEG files and save the file back to disk. Here are the commands for doing so: # Set a given attribute info.data['iptc attribute here'] = 'new value here' # Clear the keywords or supp. categories list info.keywords = [] info.supplementalCategories = [] info.contacts = [] # Add keywords or supp. categories info.keyword.append('frob') # You can also add a list reference info.keyword.extend(['frob', 'nob', 'widget']) info.keyword += ['gadget'] SAVING FILES With JPEG files you can add/change attributes, add keywords, etc., and then call: info.save() info.saveAs('new-file-name.jpg') This will save the file with the updated IPTC info. Please only run this on *copies* of your images -- not your precious originals! -- because I'm not liable for any corruption of your images. (If you read software license agreements, nobody else is liable, either. Make backups of your originals!) If you're into image wizardry, there are a couple handy options you can use on saving. One feature is to trash the Adobe block of data, which contains IPTC info, color settings, Photoshop print settings, and stuff like that. The other is to trash all application blocks, including stuff like EXIF and FlashPix data. This can be handy for reducing file sizes. The options are passed as a dict to save() and saveAs(), e.g.: info.save({'discardAdobeParts': 'on'}) info.saveAs('new-file-name.jpg', {'discardAppParts': 'on'}) Note that if there was IPTC info in the image, or you added some yourself, the new image will have an Adobe part with only the IPTC information. XML AND SQL EXPORT FEATURES IPTCInfo also allows you to easily generate XML and SQL from the image metadata. For XML, call: xml = info.exportXML('entity-name', extra-data, 'optional output file name') This returns XML containing all image metadata. Attribute names are translated into XML tags, making adjustments to spaces and slashes for compatibility. (Spaces become underbars, slashes become dashes.) You provide an entity name; all data will be contained within this entity. You can optionally provides a reference to a hash of extra data. This will get put into the XML, too. (Example: you may want to put info on the image's location into the XML.) Keys must be valid XML tag names. You can also provide a filename, and the XML will be dumped into there. For SQL, it goes like this: my mappings = { 'IPTC dataset name here': 'your table column name here', 'caption/abstract': 'caption', 'city': 'city', 'province/state': 'state} # etc etc etc. statement = info.exportSQL('mytable', mappings, extra-data) This returns a SQL statement to insert into your given table name a set of values from the image. You pass in a reference to a hash which maps IPTC dataset names into column names for the database table. As with XML export, you can also provide extra information to be stuck into the SQL. IPTC ATTRIBUTE REFERENCE object name originating program edit status program version editorial update object cycle urgency by-line subject reference by-line title category city fixture identifier sub-location content location code province/state content location name country/primary location code release date country/primary location name release time original transmission reference expiration date headline expiration time credit special instructions source action advised copyright notice reference service contact reference date caption/abstract reference number writer/editor date created image type time created image orientation digital creation date language identifier digital creation time custom1 - custom20: NOT STANDARD but used by Fotostation. IPTCInfo also supports these fields. KNOWN BUGS IPTC meta-info on MacOS may be stored in the resource fork instead of the data fork. This program will currently not scan the resource fork. I have heard that some programs will embed IPTC info at the end of the file instead of the beginning. The module will currently only look near the front of the file. If you have a file with IPTC data that IPTCInfo can't find, please contact me! I would like to ensure IPTCInfo works with everyone's files. AUTHOR Josh Carter, josh@multipart-mixed.com """ __version__ = '1.9.2-rc5' __author__ = 'Gulcsi, Tams' SURELY_WRITE_CHARSET_INFO = False from struct import pack, unpack from cStringIO import StringIO import sys, re, codecs, os class String(basestring): def __iadd__(self, other): assert isinstance(other, str) super(type(self), self).__iadd__(other) class EOFException(Exception): def __init__(self, *args): Exception.__init__(self) self._str = '\n'.join(args) def __str__(self): return self._str def push(diction, key, value): if diction.has_key(key) and isinstance(diction[key], list): diction[key].append(value) else: diction[key] = value def duck_typed(obj, prefs): if isinstance(prefs, basestring): prefs = [prefs] for pref in prefs: if not hasattr(obj, pref): return False return True #~ sys_enc = sys.getfilesystemencoding() sys_enc = "utf_8" # Debug off for production use debugMode = 0 ##################################### # These names match the codes defined in ITPC's IIM record 2. # This hash is for non-repeating data items; repeating ones # are in %listdatasets below. c_datasets = { # 0: 'record version', # skip -- binary data 5: 'object name', 7: 'edit status', 8: 'editorial update', 10: 'urgency', 12: 'subject reference', 15: 'category', 20: 'supplemental category', 22: 'fixture identifier', 25: 'keywords', 26: 'content location code', 27: 'content location name', 30: 'release date', 35: 'release time', 37: 'expiration date', 38: 'expiration time', 40: 'special instructions', 42: 'action advised', 45: 'reference service', 47: 'reference date', 50: 'reference number', 55: 'date created', 60: 'time created', 62: 'digital creation date', 63: 'digital creation time', 65: 'originating program', 70: 'program version', 75: 'object cycle', 80: 'by-line', 85: 'by-line title', 90: 'city', 92: 'sub-location', 95: 'province/state', 100: 'country/primary location code', 101: 'country/primary location name', 103: 'original transmission reference', 105: 'headline', 110: 'credit', 115: 'source', 116: 'copyright notice', 118: 'contact', 120: 'caption/abstract', 122: 'writer/editor', # 125: 'rasterized caption', # unsupported (binary data) 130: 'image type', 131: 'image orientation', 135: 'language identifier', 200: 'custom1', # These are NOT STANDARD, but are used by 201: 'custom2', # Fotostation. Use at your own risk. They're 202: 'custom3', # here in case you need to store some special 203: 'custom4', # stuff, but note that other programs won't 204: 'custom5', # recognize them and may blow them away if 205: 'custom6', # you open and re-save the file. (Except with 206: 'custom7', # Fotostation, of course.) 207: 'custom8', 208: 'custom9', 209: 'custom10', 210: 'custom11', 211: 'custom12', 212: 'custom13', 213: 'custom14', 214: 'custom15', 215: 'custom16', 216: 'custom17', 217: 'custom18', 218: 'custom19', 219: 'custom20', } c_datasets_r = dict([(v, k) for k, v in c_datasets.iteritems()]) class IPTCData(dict): """Dict with int/string keys from c_listdatanames""" def __init__(self, diction={}, *args, **kwds): super(type(self), self).__init__(self, *args, **kwds) self.update(dict([(self.keyAsInt(k), v) for k, v in diction.iteritems()])) c_cust_pre = 'nonstandard_' def keyAsInt(self, key): global c_datasets_r if isinstance(key, int): return key #and c_datasets.has_key(key): return key elif c_datasets_r.has_key(key): return c_datasets_r[key] elif (key.startswith(self.c_cust_pre) and key[len(self.c_cust_pre):].isdigit()): return int(key[len(self.c_cust_pre):]) else: raise KeyError("Key %s is not in %s!" % (key, c_datasets_r.keys())) def keyAsStr(self, key): global c_datasets if isinstance(key, basestring) and c_datasets_r.has_key(key): return key elif c_datasets.has_key(key): return c_datasets[key] elif isinstance(key, int): return self.c_cust_pre + str(key) else: raise KeyError("Key %s is not in %s!" % (key, c_datasets.keys())) def has_key(self, name): return super(type(self), self).has_key(self.keyAsInt(name)) def __getitem__(self, name): return super(type(self), self).get(self.keyAsInt(name), None) def __setitem__(self, name, value): key = self.keyAsInt(name) o = super(type(self), self) if o.has_key(key) and isinstance(o.__getitem__(key), list): #print key, c_datasets[key], o.__getitem__(key) if isinstance(value, list): o.__setitem__(key, value) else: raise ValueError("For %s only lists acceptable!" % name) else: o.__setitem__(self.keyAsInt(name), value) def debug(level, *args): if level < debugMode: print '\n'.join(map(unicode, args)) def _getSetSomeList(name): def getList(self): """Returns the list of %s.""" % name return self._data[name] def setList(self, value): """Sets the list of %s.""" % name if isinstance(value, (list, tuple)): self._data[name] = list(value) elif isinstance(value, basestring): self._data[name] = [value] print 'Warning: IPTCInfo.%s is a list!' % name else: raise ValueError('IPTCInfo.%s is a list!' % name) return (getList, setList) class IPTCInfo(object): """info = IPTCInfo('image filename goes here') File can be a file-like object or a string. If it is a string, it is assumed to be a filename. Returns IPTCInfo object filled with metadata from the given image file. File on disk will be closed, and changes made to the IPTCInfo object will *not* be flushed back to disk. If force==True, than forces an object to always be returned. This allows you to start adding stuff to files that don't have IPTC info and then save it.""" def __init__(self, fobj, force=False, *args, **kwds): # Open file and snarf data from it. self.error = None self._data = IPTCData({'supplemental category': [], 'keywords': [], 'contact': []}) if duck_typed(fobj, 'read'): self._filename = None self._fh = fobj else: self._filename = fobj fh = self._getfh() self.inp_charset = sys_enc self.out_charset = 'utf_8' datafound = self.scanToFirstIMMTag(fh) if datafound or force: # Do the real snarfing here if datafound: self.collectIIMInfo(fh) else: self.log("No IPTC data found.") self._closefh(fh) raise Exception("No IPTC data found.") self._closefh(fh) def _closefh(self, fh): if fh and self._filename is not None: fh.close() def _getfh(self, mode='r'): assert self._filename is not None or self._fh is not None if self._filename is not None: fh = file(self._filename, (mode + 'b').replace('bb', 'b')) if not fh: self.log("Can't open file") return None else: return fh else: return self._fh ####################################################################### # New, Save, Destroy, Error ####################################################################### def error(self): """Returns the last error message""" return self.error def save(self, options=None): """Saves Jpeg with IPTC data back to the same file it came from.""" assert self._filename is not None return self.saveAs(self._filename, options) def _filepos(self, fh): fh.flush() return 'POS=%d\n' % fh.tell() def saveAs(self, newfile, options=None): """Saves Jpeg with IPTC data to a given file name.""" assert self._filename is not None # Open file and snarf data from it. fh = self._getfh() if not self.fileIsJpeg(fh): self.log("Source file is not a Jpeg; I can only save Jpegs. Sorry.") return None ret = self.jpegCollectFileParts(fh, options) self._closefh(fh) if ret is None: self.log("collectfileparts failed") raise Exception('collectfileparts failed') (start, end, adobe) = ret debug(2, 'start: %d, end: %d, adobe:%d' % tuple(map(len, ret))) self.hexDump(start), len(end) debug(3, 'adobe1', adobe) if options is not None and options.has_key('discardAdobeParts'): adobe = None debug(3, 'adobe2', adobe) debug(1, 'writing...') # fh = os.tmpfile() ## 20051011 - Windows doesn't like tmpfile ## # Open dest file and stuff data there # fh.truncate() # fh.seek(0, 0) # debug(2, self._filepos(fh)) fh = StringIO() if not fh: self.log("Can't open output file") return None debug(3, len(start), len(end)) fh.write(start) # character set ch = self.c_charset_r.get((self.out_charset is None and [self.inp_charset] or [self.out_charset])[0], None) # writing the character set is not the best practice - couldn't find the needed place (record) for it yet! if SURELY_WRITE_CHARSET_INFO and ch is not None: fh.write(pack("!BBBHH", 0x1c, 1, 90, 4, ch)) debug(2, self._filepos(fh)) #$self->PhotoshopIIMBlock($adobe, $self->PackedIIMData()); data = self.photoshopIIMBlock(adobe, self.packedIIMData()) debug(3, len(data), self.hexDump(data)) fh.write(data) debug(2, self._filepos(fh)) fh.flush() fh.write(end) debug(2, self._filepos(fh)) fh.flush() #copy the successfully written file back to the given file fh2 = file(newfile, 'wb') fh2.truncate() fh2.seek(0,0) fh.seek(0, 0) while 1: buf = fh.read(8192) if buf is None or len(buf) == 0: break fh2.write(buf) self._closefh(fh) fh2.flush() fh2.close() return True def __destroy__(self): """Called when object is destroyed. No action necessary in this case.""" pass ####################################################################### # Attributes for clients ####################################################################### def getData(self): return self._data def setData(self, value): raise Exception('You cannot overwrite the data, only its elements!') data = property(getData, setData) keywords = property(*_getSetSomeList('keywords')) supplementalCategories = property(*_getSetSomeList('supplemental category')) contacts = property(*_getSetSomeList('contact')) def __str__(self): return ('charset: ' + self.inp_charset + '\n' + str(dict([(self._data.keyAsStr(k), v) for k, v in self._data.iteritems()]))) def readExactly(self, fh, length): """readExactly Reads exactly length bytes and throws an exception if EOF is hitten before. """ ## assert isinstance(fh, file) assert duck_typed(fh, 'read') # duck typing buf = fh.read(length) if buf is None or len(buf) < length: raise EOFException('readExactly: %s' % str(fh)) return buf def seekExactly(self, fh, length): """seekExactly Seeks length bytes from the current position and checks the result """ ## assert isinstance(fh, file) assert duck_typed(fh, ['seek', 'tell']) # duck typing pos = fh.tell() fh.seek(length, 1) if fh.tell() - pos != length: raise EOFException() ####################################################################### # XML, SQL export ####################################################################### def exportXML(self, basetag, extra, filename): """xml = info.exportXML('entity-name', extra-data, 'optional output file name') Exports XML containing all image metadata. Attribute names are translated into XML tags, making adjustments to spaces and slashes for compatibility. (Spaces become underbars, slashes become dashes.) Caller provides an entity name; all data will be contained within this entity. Caller optionally provides a reference to a hash of extra data. This will be output into the XML, too. Keys must be valid XML tag names. Optionally provide a filename, and the XML will be dumped into there.""" P = lambda s: ' '*off + s + '\n' off = 0 if len(basetag) == 0: basetag = 'photo' out = P("<%s>" % basetag) off += 1 # dump extra info first, if any for k, v in (isinstance(extra, dict) and [extra] or [{}])[0].iteritems(): out += P("<%s>%s" % (k, v, k)) # dump our stuff for k, v in self._data.iteritems(): if not isinstance(v, list): key = re.sub('/', '-', re.sub(' +', ' ', self._data.keyAsStr(k))) out += P("<%s>%s" % (key, v, key)) # print keywords kw = self.keywords() if kw and len(kw) > 0: out += P("") off += 1 for k in kw: out += P("%s" % k) off -= 1 out += P("") # print supplemental categories sc = self.supplementalCategories() if sc and len(sc) > 0: out += P("") off += 1 for k in sc: out += P("%s" % k) off -= 1 out += P("") # print contacts kw = self.contacts() if kw and len(kw) > 0: out += P("") off += 1 for k in kw: out += P("%s" % k) off -= 1 out += P("") # close base tag off -= 1 out += P("" % basetag) # export to file if caller asked for it. if len(filename) > 0: xmlout = file(filename, 'wb') xmlout.write(out) xmlout.close() return out def exportSQL(self, tablename, mappings, extra): """statement = info.exportSQL('mytable', mappings, extra-data) mappings = { 'IPTC dataset name here': 'your table column name here', 'caption/abstract': 'caption', 'city': 'city', 'province/state': 'state} # etc etc etc. Returns a SQL statement to insert into your given table name a set of values from the image. Caller passes in a reference to a hash which maps IPTC dataset names into column names for the database table. Optionally pass in a ref to a hash of extra data which will also be included in the insert statement. Keys in that hash must be valid column names.""" if (tablename is None or mappings is None): return None statement = columns = values = None E = lambda s: "'%s'" % re.sub("'", "''", s) # escape single quotes # start with extra data, if any columns = ', '.join(extra.keys() + mappings.keys()) values = ', '.join(map(E, extra.values() + [self.getdata(k) for k in mappings.keys()])) # process our data statement = "INSERT INTO %s (%s) VALUES (%s)" \ % (tablename, columns, values) return statement ####################################################################### # File parsing functions (private) ####################################################################### def scanToFirstIMMTag(self, fh): #OK# """Scans to first IIM Record 2 tag in the file. The will either use smart scanning for Jpegs or blind scanning for other file types.""" ## assert isinstance(fh, file) if self.fileIsJpeg(fh): self.log("File is Jpeg, proceeding with JpegScan") return self.jpegScan(fh) else: self.log("File not a JPEG, trying BlindScan") return self.blindScan(fh) def fileIsJpeg(self, fh): #OK# """Checks to see if this file is a Jpeg/JFIF or not. Will reset the file position back to 0 after it's done in either case.""" # reset to beginning just in case ## assert isinstance(fh, file) assert duck_typed(fh, ['read', 'seek']) fh.seek(0, 0) if debugMode > 0: self.log("Opening 16 bytes of file:\n"); dump = fh.read(16) debug(3, self.hexDump(dump)) fh.seek(0, 0) # check start of file marker ered = False try: (ff, soi) = fh.read(2) if not (ord(ff) == 0xff and ord(soi) == 0xd8): ered = False else: # now check for APP0 marker. I'll assume that anything with a SOI # followed by APP0 is "close enough" for our purposes. (We're not # dinking with image data, so anything following the Jpeg tagging # system should work.) (ff, app0) = fh.read(2) if not (ord(ff) == 0xff): ered = False else: ered = True finally: # reset to beginning of file fh.seek(0, 0) return ered c_marker_err = {0: "Marker scan failed", 0xd9: "Marker scan hit end of image marker", 0xda: "Marker scan hit start of image data"} def jpegScan(self, fh): #OK# """Assuming the file is a Jpeg (see above), this will scan through the markers looking for the APP13 marker, where IPTC/IIM data should be found. While this isn't a formally defined standard, all programs have (supposedly) adopted Adobe's technique of putting the data in APP13.""" # Skip past start of file marker ## assert isinstance(fh, file) try: (ff, soi) = self.readExactly(fh, 2) except EOFException: return None if not (ord(ff) == 0xff and ord(soi) == 0xd8): self.error = "JpegScan: invalid start of file" self.log(self.error) return None # Scan for the APP13 marker which will contain our IPTC info (I hope). while 1: err = None marker = self.jpegNextMarker(fh) if ord(marker) == 0xed: break #237 err = self.c_marker_err.get(ord(marker), None) if err is None and self.jpegSkipVariable(fh) == 0: err = "JpegSkipVariable failed" if err is not None: self.error = err self.log(err) return None # If were's here, we must have found the right marker. Now # BlindScan through the data. return self.blindScan(fh, MAX=self.jpegGetVariableLength(fh)) def jpegNextMarker(self, fh): #OK# """Scans to the start of the next valid-looking marker. Return value is the marker id.""" ## assert isinstance(fh, file) # Find 0xff byte. We should already be on it. try: byte = self.readExactly(fh, 1) except EOFException: return None while ord(byte) != 0xff: self.log("JpegNextMarker: warning: bogus stuff in Jpeg file"); try: byte = self.readExactly(fh, 1) except EOFException: return None # Now skip any extra 0xffs, which are valid padding. while 1: try: byte = self.readExactly(fh, 1) except EOFException: return None if ord(byte) != 0xff: break # byte should now contain the marker id. self.log("JpegNextMarker: at marker %02X (%d)" % (ord(byte), ord(byte))) return byte def jpegGetVariableLength(self, fh): #OK# """Gets length of current variable-length section. File position at start must be on the marker itself, e.g. immediately after call to JPEGNextMarker. File position is updated to just past the length field.""" ## assert isinstance(fh, file) try: length = unpack('!H', self.readExactly(fh, 2))[0] except EOFException: return 0 self.log('JPEG variable length: %d' % length) # Length includes itself, so must be at least 2 if length < 2: self.log("JPEGGetVariableLength: erroneous JPEG marker length") return 0 return length-2 def jpegSkipVariable(self, fh, rSave=None): #OK# """Skips variable-length section of Jpeg block. Should always be called between calls to JpegNextMarker to ensure JpegNextMarker is at the start of data it can properly parse.""" ## assert isinstance(fh, file) # Get the marker parameter length count length = self.jpegGetVariableLength(fh) if length == 0: return None # Skip remaining bytes if rSave is not None or debugMode > 0: try: temp = self.readExactly(fh, length) except EOFException: self.log("JpegSkipVariable: read failed while skipping var data"); return None # prints out a heck of a lot of stuff # self.hexDump(temp) else: # Just seek try: self.seekExactly(fh, length) except EOFException: self.log("JpegSkipVariable: read failed while skipping var data"); return None return (rSave is not None and [temp] or [True])[0] c_charset = {100: 'iso8859_1', 101: 'iso8859_2', 109: 'iso8859_3', 110: 'iso8859_4', 111: 'iso8859_5', 125: 'iso8859_7', 127: 'iso8859_6', 138: 'iso8859_8', 196: 'utf_8'} c_charset_r = dict([(v, k) for k, v in c_charset.iteritems()]) def blindScan(self, fh, MAX=8192): #OK# """Scans blindly to first IIM Record 2 tag in the file. This method may or may not work on any arbitrary file type, but it doesn't hurt to check. We expect to see this tag within the first 8k of data. (This limit may need to be changed or eliminated depending on how other programs choose to store IIM.)""" ## assert isinstance(fh, file) assert duck_typed(fh, 'read') offset = 0 # keep within first 8192 bytes # NOTE: this may need to change self.log('blindScan: starting scan, max length %d' % MAX) # start digging while offset <= MAX: try: temp = self.readExactly(fh, 1) except EOFException: self.log("BlindScan: hit EOF while scanning"); return None # look for tag identifier 0x1c if ord(temp) == 0x1c: # if we found that, look for record 2, dataset 0 # (record version number) (record, dataset) = fh.read(2) if ord(record) == 1 and ord(dataset) == 90: # found character set's record! try: temp = self.readExactly(fh, self.jpegGetVariableLength(fh)) self.inp_charset = self.c_charset.get(unpack('!H', temp)[0], sys_enc) self.log("BlindScan: found character set '%s' at offset %d" % (self.inp_charset, offset)) except EOFException: pass elif ord(record) == 2: # found it. seek to start of this tag and return. self.log("BlindScan: found IIM start at offset %d" % offset); try: self.seekExactly(fh, -3) # seek rel to current position except EOFException: return None return offset else: # didn't find it. back up 2 to make up for # those reads above. try: self.seekExactly(fh, -2) # seek rel to current position except EOFException: return None # no tag, keep scanning offset += 1 return False def collectIIMInfo(self, fh): #OK# """Assuming file is seeked to start of IIM data (using above), this reads all the data into our object's hashes""" # NOTE: file should already be at the start of the first # IPTC code: record 2, dataset 0. ## assert isinstance(fh, file) assert duck_typed(fh, 'read') while 1: try: header = self.readExactly(fh, 5) except EOFException: return None (tag, record, dataset, length) = unpack("!BBBH", header) # bail if we're past end of IIM record 2 data if not (tag == 0x1c and record == 2): return None alist = {'tag': tag, 'record': record, 'dataset': dataset, 'length': length} debug(1, '\n'.join(['%s\t: %s' % (k, v) for k, v in alist.iteritems()])) value = fh.read(length) try: value = unicode(value, encoding=self.inp_charset, errors='strict') except: self.log('Data "%s" is not in encoding %s!' % (value, self.inp_charset)) #value = unicode(value, encoding=self.inp_charset, errors='replace') value = unicode(value, encoding="iso-8859-1", errors='replace') # try to extract first into _listdata (keywords, categories) # and, if unsuccessful, into _data. Tags which are not in the # current IIM spec (version 4) are currently discarded. if self._data.has_key(dataset) and isinstance(self._data[dataset], list): self._data[dataset] += [value] elif dataset != 0: self._data[dataset] = value ####################################################################### # File Saving ####################################################################### def jpegCollectFileParts(self, fh, discardAppParts=False): """Collects all pieces of the file except for the IPTC info that we'll replace when saving. Returns the stuff before the info, stuff after, and the contents of the Adobe Resource Block that the IPTC data goes in. Returns None if a file parsing error occured.""" ## assert isinstance(fh, file) assert duck_typed(fh, ['seek', 'read']) adobeParts = '' start = '' # Start at beginning of file fh.seek(0, 0) # Skip past start of file marker (ff, soi) = fh.read(2) if not (ord(ff) == 0xff and ord(soi) == 0xd8): self.error = "JpegScan: invalid start of file" self.log(self.error) return None # Begin building start of file start += pack("BB", 0xff, 0xd8) # Get first marker in file. This will be APP0 for JFIF or APP1 for # EXIF. marker = self.jpegNextMarker(fh) app0data = '' app0data = self.jpegSkipVariable(fh, app0data) if app0data is None: self.error = 'jpegSkipVariable failed' self.log(error) return None if ord(marker) == 0xe0 or not discardAppParts: # Always include APP0 marker at start if it's present. start += pack('BB', 0xff, ord(marker)) # Remember that the length must include itself (2 bytes) start += pack('!H', len(app0data)+2) start += app0data else: # Manually insert APP0 if we're trashing application parts, since # all JFIF format images should start with the version block. debug(2, 'discardAppParts=', discardAppParts) start += pack("BB", 0xff, 0xe0) start += pack("!H", 16) # length (including these 2 bytes) start += "JFIF" # format start += pack("BB", 1, 2) # call it version 1.2 (current JFIF) start += pack('8B', 0) # zero everything else # Now scan through all markers in file until we hit image data or # IPTC stuff. end = '' while 1: marker = self.jpegNextMarker(fh) if marker is None or ord(marker) == 0: self.error = "Marker scan failed" self.log(self.error) return None # Check for end of image elif ord(marker) == 0xd9: self.log("JpegCollectFileParts: saw end of image marker") end += pack("BB", 0xff, ord(marker)) break # Check for start of compressed data elif ord(marker) == 0xda: self.log("JpegCollectFileParts: saw start of compressed data") end += pack("BB", 0xff, ord(marker)) break partdata = '' partdata = self.jpegSkipVariable(fh, partdata) if not partdata: self.error = "JpegSkipVariable failed" self.log(self.error) return None partdata = str(partdata) # Take all parts aside from APP13, which we'll replace # ourselves. if (discardAppParts and ord(marker) >= 0xe0 and ord(marker) <= 0xef): # Skip all application markers, including Adobe parts adobeParts = '' elif ord(marker) == 0xed: # Collect the adobe stuff from part 13 adobeParts = self.collectAdobeParts(partdata) break else: # Append all other parts to start section start += pack("BB", 0xff, ord(marker)) start += pack("!H", len(partdata) + 2) start += partdata # Append rest of file to end while 1: buff = fh.read() if buff is None or len(buff) == 0: break end += buff return (start, end, adobeParts) def collectAdobeParts(self, data): """Part APP13 contains yet another markup format, one defined by Adobe. See"File Formats Specification" in the Photoshop SDK (avail from www.adobe.com). We must take everything but the IPTC data so that way we can write the file back without losing everything else Photoshop stuffed into the APP13 block.""" assert isinstance(data, basestring) length = len(data) offset = 0 out = '' # Skip preamble offset = len('Photoshop 3.0 ') # Process everything while offset < length: # Get OSType and ID (ostype, id1, id2) = unpack("!LBB", data[offset:offset+6]) offset += 6 # Get pascal string stringlen = unpack("B", data[offset:offset+1])[0] offset += 1 string = data[offset:offset+stringlen] offset += stringlen # round up if odd if (stringlen % 2 != 0): offset += 1 # there should be a null if string len is 0 if stringlen == 0: offset += 1 # Get variable-size data size = unpack("!L", data[offset:offset+4])[0] offset += 4 var = data[offset:offset+size] offset += size if size % 2 != 0: offset += 1 # round up if odd # skip IIM data (0x0404), but write everything else out if not (id1 == 4 and id2 == 4): out += pack("!LBB", ostype, id1, id2) out += pack("B", stringlen) out += string if stringlen == 0 or stringlen % 2 != 0: out += pack("B", 0) out += pack("!L", size) out += var if size % 2 != 0 and len(out) % 2 != 0: out += pack("B", 0) return out def _enc(self, text): """Recodes the given text from the old character set to utf-8""" res = text out_charset = (self.out_charset is None and [self.inp_charset] or [self.out_charset])[0] if isinstance(text, unicode): res = text.encode(out_charset) elif isinstance(text, str): try: res = unicode(text, encoding=self.inp_charset).encode(out_charset) except: self.log("_enc: charset %s is not working for %s" % (self.inp_charset, text)) res = unicode(text, encoding=self.inp_charset, errors='replace' ).encode(out_charset) elif isinstance(text, (list, tuple)): res = type(text)(map(self._enc, text)) return res def packedIIMData(self): """Assembles and returns our _data and _listdata into IIM format for embedding into an image.""" out = '' (tag, record) = (0x1c, 0x02) # Print record version # tag - record - dataset - len (short) - 4 (short) out += pack("!BBBHH", tag, record, 0, 2, 4) debug(3, self.hexDump(out)) # Iterate over data sets for dataset, value in self._data.iteritems(): if len(value) == 0: continue if not (c_datasets.has_key(dataset) or isinstance(dataset, int)): self.log("PackedIIMData: illegal dataname '%s' (%d)" % (c_datasets[dataset], dataset)) continue value = self._enc(value) #~ print value if not isinstance(value, list): value = str(value) out += pack("!BBBH", tag, record, dataset, len(value)) out += value else: for v in map(str, value): out += pack("!BBBH", tag, record, dataset, len(v)) out += v return out def photoshopIIMBlock(self, otherparts, data): """Assembles the blob of Photoshop "resource data" that includes our fresh IIM data (from PackedIIMData) and the other Adobe parts we found in the file, if there were any.""" out = '' assert isinstance(data, basestring) resourceBlock = "Photoshop 3.0" resourceBlock += pack("B", 0) # Photoshop identifier resourceBlock += "8BIM" # 0x0404 is IIM data, 00 is required empty string resourceBlock += pack("BBBB", 0x04, 0x04, 0, 0) # length of data as 32-bit, network-byte order resourceBlock += pack("!L", len(data)) # Now tack data on there resourceBlock += data # Pad with a blank if not even size if len(data) % 2 != 0: resourceBlock += pack("B", 0) # Finally tack on other data if otherparts is not None: resourceBlock += otherparts out += pack("BB", 0xff, 0xed) # Jpeg start of block, APP13 out += pack("!H", len(resourceBlock) + 2) # length out += resourceBlock return out ####################################################################### # Helpers, docs ####################################################################### def log(self, string): """log: just prints a message to STDERR if debugMode is on.""" if debugMode > 0: sys.stderr.write("**IPTC** %s\n" % string) def hexDump(self, dump): """Very helpful when debugging.""" length = len(dump) P = lambda z: ((ord(z) >= 0x21 and ord(z) <= 0x7e) and [z] or ['.'])[0] ROWLEN = 18 ered = '\n' for j in range(0, length/ROWLEN + int(length%ROWLEN>0)): row = dump[j*ROWLEN:(j+1)*ROWLEN] ered += ('%02X '*len(row) + ' '*(ROWLEN-len(row)) + '| %s\n') % \ tuple(map(ord, row) + [''.join(map(P, row))]) return ered def jpegDebugScan(filename): """Also very helpful when debugging.""" assert isinstance(filename, basestring) and os.path.isfile(filename) fh = file(filename, 'wb') if not fh: raise Exception("Can't open %s" % filename) # Skip past start of file marker (ff, soi) = fh.read(2) if not (ord(ff) == 0xff and ord(soi) == 0xd8): self.log("JpegScan: invalid start of file") else: # scan to 0xDA (start of scan), dumping the markers we see between # here and there. while 1: marker = self.jpegNextMarker(fh) if ord(marker) == 0xda: break if ord(marker) == 0: self.log("Marker scan failed") break elif ord(marker) == 0xd9: self.log("Marker scan hit end of image marker") break if not self.jpegSkipVariable(fh): self.log("JpegSkipVariable failed") return None self._closefh(fh) if __name__ == '__main__': if len(sys.argv) > 1: info = IPTCInfo(sys.argv[1]) print info postr-0.12.4/src/postr.py0000644000175000017500000010741511274424736013771 0ustar gpoogpoo# Postr, a Flickr Uploader # # Copyright (C) 2006-2008 Ross Burton # # 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 2, 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, write to the Free Software Foundation, Inc., 51 Franklin # St, Fifth Floor, Boston, MA 02110-1301 USA import logging, os, urllib from urlparse import urlparse from os.path import basename import pygtk; pygtk.require ("2.0") import gobject, gtk, gtk.glade, gconf from AboutDialog import AboutDialog from AuthenticationDialog import AuthenticationDialog from ProgressDialog import ProgressDialog from ErrorDialog import ErrorDialog import ImageStore, ImageList, StatusBar, PrivacyCombo, SafetyCombo, GroupSelector from flickrest import Flickr from twisted.web.client import getPage import EXIF from iptcinfo import IPTCInfo from util import * try: from gtkunique import UniqueApp except ImportError: from DummyUnique import UniqueApp #logging.basicConfig(level=logging.DEBUG) # Exif information about image orientation (ROTATED_0, ROTATED_180, ROTATED_90_CW, ROTATED_90_CCW ) = (1, 3, 6, 8) class Postr (UniqueApp): def __init__(self): UniqueApp.__init__(self, 'com.burtonini.Postr') try: self.connect("message", self.on_message) except AttributeError: pass self.is_connected = False self.flickr = Flickr(api_key="c53cebd15ed936073134cec858036f1d", secret="7db1b8ef68979779", perms="write") gtk.window_set_default_icon_name("postr") gtk.glade.set_custom_handler(self.get_custom_handler) glade = gtk.glade.XML(os.path.join (os.path.dirname(__file__), "postr.glade")) glade.signal_autoconnect(self) get_glade_widgets (glade, self, ("window", "upload_menu", "upload_button", "statusbar", "thumbnail_image", "title_entry", "desc_view", "tags_entry", "set_combo", "group_selector", "privacy_combo", "safety_combo", "visible_check", "thumbview") ) align_labels(glade, ("title_label", "desc_label", "tags_label", "set_label", "privacy_label", "safety_label")) # Just for you, Daniel. try: if os.getlogin() == "daniels": self.window.set_title("Respecognise") except Exception: pass self.model = ImageStore.ImageStore () self.model.connect("row-inserted", self.on_model_changed) self.model.connect("row-deleted", self.on_model_changed) self.thumbview.set_model(self.model) self.thumbview.connect("drag_data_received", self.on_drag_data_received) selection = self.thumbview.get_selection() selection.connect("changed", self.on_selection_changed) # TODO: remove this self.current_it = None self.last_folder = None self.upload_quota = None self.thumbnail_image.clear() self.thumbnail_image.set_size_request(128, 128) self.change_signals = [] # List of (widget, signal ID) tuples self.change_signals.append((self.title_entry, self.title_entry.connect('changed', self.on_field_changed, ImageStore.COL_TITLE))) self.change_signals.append((self.desc_view.get_buffer(), self.desc_view.get_buffer().connect('changed', self.on_field_changed, ImageStore.COL_DESCRIPTION))) self.change_signals.append((self.tags_entry, self.tags_entry.connect('changed', self.on_field_changed, ImageStore.COL_TAGS))) self.change_signals.append((self.group_selector, self.group_selector.connect('changed', self.on_field_changed, ImageStore.COL_GROUPS))) self.change_signals.append((self.privacy_combo, self.privacy_combo.connect('changed', self.on_field_changed, ImageStore.COL_PRIVACY))) self.change_signals.append((self.safety_combo, self.safety_combo.connect('changed', self.on_field_changed, ImageStore.COL_SAFETY))) self.change_signals.append((self.visible_check, self.visible_check.connect('toggled', self.on_field_changed, ImageStore.COL_VISIBLE))) self.thumbnail_image.connect('size-allocate', self.update_thumbnail) self.old_thumb_allocation = None # The set selector combo self.sets = gtk.ListStore (gobject.TYPE_STRING, # ID gobject.TYPE_STRING, # Name gtk.gdk.Pixbuf) # Thumbnail self.sets.set (self.sets.append(), 0, None, 1, "None") self.set_combo.set_model (self.sets) self.set_combo.set_active (-1) self.on_selection_changed(selection) renderer = gtk.CellRendererPixbuf() self.set_combo.pack_start (renderer, expand=False) self.set_combo.set_attributes(renderer, pixbuf=2) renderer = gtk.CellRendererText() self.set_combo.pack_start (renderer, expand=False) self.set_combo.set_attributes(renderer, text=1) # The upload progress dialog self.uploading = False self.current_upload_it = None self.cancel_upload = False def cancel(): self.cancel_upload = True self.progress_dialog = ProgressDialog(cancel) self.progress_dialog.set_transient_for(self.window) # Disable the Upload menu until the user has authenticated self.update_upload() # Update the proxy configuration client = gconf.client_get_default() client.add_dir("/system/http_proxy", gconf.CLIENT_PRELOAD_RECURSIVE) client.notify_add("/system/http_proxy", self.proxy_changed) self.proxy_changed(client, 0, None, None) # Connect to flickr, go go go self.flickr.authenticate_1().addCallbacks(self.auth_open_url, self.twisted_error) def twisted_error(self, failure): self.update_upload() dialog = ErrorDialog(self.window) dialog.set_from_failure(failure) dialog.show_all() def proxy_changed(self, client, cnxn_id, entry, something): if client.get_bool("/system/http_proxy/use_http_proxy"): host = client.get_string("/system/http_proxy/host") port = client.get_int("/system/http_proxy/port") if host is None or host == "" or port == 0: self.flickr.set_proxy(None) return if client.get_bool("/system/http_proxy/use_authentication"): user = client.get_string("/system/http_proxy/authentication_user") password = client.get_string("/system/http_proxy/authentication_password") if user and user != "": url = "http://%s:%s@%s:%d" % (user, password, host, port) else: url = "http://%s:%d" % (host, port) else: url = "http://%s:%d" % (host, port) self.flickr.set_proxy(url) else: self.flickr.set_proxy(None) def get_custom_handler(self, glade, function_name, widget_name, str1, str2, int1, int2): """libglade callback to create custom widgets.""" handler = getattr(self, function_name, None) if handler: return handler(str1, str2, int1, int2) else: widget = eval(function_name) widget.show() return widget def group_selector_new (self, *args): w = GroupSelector.GroupSelector(self.flickr) w.show() return w def image_list_new (self, *args): """Custom widget creation function to make the image list.""" view = ImageList.ImageList () view.show() return view def status_bar_new (self, *args): bar = StatusBar.StatusBar(self.flickr) bar.show() return bar def on_message(self, app, command, command_data, startup_id, screen, workspace): """Callback from UniqueApp, when a message arrives.""" if command == gtkunique.OPEN: self.add_image_filename(command_data) return gtkunique.RESPONSE_OK else: return gtkunique.RESPONSE_ABORT def on_model_changed(self, *args): # We don't care about the arguments, because we just want to know when # the model was changed, not what was changed. self.update_upload() def auth_open_url(self, state): """Callback from midway through Flickr authentication. At this point we either have cached tokens so can carry on, or need to open a web browser to authenticate the user.""" if state is None: self.connected(True) else: dialog = AuthenticationDialog(self.window, state['url']) if dialog.run() == gtk.RESPONSE_ACCEPT: self.flickr.authenticate_2(state).addCallbacks(self.connected, self.twisted_error) dialog.destroy() def connected(self, connected): """Callback when the Flickr authentication completes.""" self.is_connected = connected if connected: self.update_upload() self.statusbar.update_quota() self.group_selector.update() self.flickr.photosets_getList().addCallbacks(self.got_photosets, self.twisted_error) def update_upload(self): connected = self.is_connected and self.model.iter_n_children(None) > 0 self.upload_menu.set_sensitive(connected) self.upload_button.set_sensitive(connected) def update_statusbar(self): """Recalculate how much is to be uploaded, and update the status bar.""" size = 0 for row in self.model: size += row[ImageStore.COL_SIZE] self.statusbar.set_upload(size) def got_set_thumb(self, page, it): loader = gtk.gdk.PixbufLoader() loader.set_size (32, 32) loader.write(page) loader.close() self.sets.set (it, 2, loader.get_pixbuf()) def got_photosets(self, rsp): """Callback for the photosets.getList call""" for photoset in rsp.findall("photosets/photoset"): it = self.sets.append() self.sets.set (it, 0, photoset.get("id"), 1, photoset.find("title").text) url = "http://static.flickr.com/%s/%s_%s%s.jpg" % (photoset.get("server"), photoset.get("primary"), photoset.get("secret"), "_s") getPage (url).addCallback (self.got_set_thumb, it).addErrback(self.twisted_error) def on_field_changed(self, widget, column): """Callback when the entry fields are changed.""" if isinstance(widget, gtk.Entry) or isinstance(widget, gtk.TextBuffer): value = widget.get_property("text") elif isinstance(widget, gtk.ToggleButton): value = widget.get_active() elif isinstance(widget, gtk.ComboBox): value = widget.get_active_iter() elif isinstance(widget, GroupSelector.GroupSelector): value = widget.get_selected_groups() else: raise "Unhandled widget type %s" % widget selection = self.thumbview.get_selection() (model, items) = selection.get_selected_rows() for path in items: it = self.model.get_iter(path) self.model.set_value (it, column, value) # TODO: remove this and use the field-changed logic def on_set_combo_changed(self, combo): """Callback when the set combo is changed.""" set_it = self.set_combo.get_active_iter() selection = self.thumbview.get_selection() (model, items) = selection.get_selected_rows() for path in items: it = self.model.get_iter(path) self.model.set_value (it, ImageStore.COL_SET, set_it) def on_add_photos_activate(self, widget): """Callback from the File->Add Photos menu item or Add button.""" dialog = gtk.FileChooserDialog(title=_("Add Photos"), parent=self.window, action=gtk.FILE_CHOOSER_ACTION_OPEN, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)) dialog.set_select_multiple(True) if self.last_folder: dialog.set_current_folder(self.last_folder) # Add filters for all reasonable image types filters = gtk.FileFilter() filters.set_name(_("Images")) filters.add_mime_type("image/*") dialog.add_filter(filters) filters = gtk.FileFilter() filters.set_name(_("All Files")) filters.add_pattern("*") dialog.add_filter(filters) # Add a preview widget preview = gtk.Image() dialog.set_preview_widget(preview) def update_preview_cb(file_chooser, preview): filename = file_chooser.get_preview_filename() try: pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(filename, 128, 128) preview.set_from_pixbuf(pixbuf) have_preview = True except: have_preview = False file_chooser.set_preview_widget_active(have_preview) dialog.connect("update-preview", update_preview_cb, preview) if dialog.run() == gtk.RESPONSE_OK: dialog.hide() self.last_folder = dialog.get_current_folder() for f in dialog.get_filenames(): self.add_image_filename(f) dialog.destroy() def on_quit_activate(self, widget, *args): """Callback from File->Quit.""" if self.uploading: dialog = gtk.MessageDialog(type=gtk.MESSAGE_WARNING, parent=self.window) dialog.add_buttons(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_QUIT, gtk.RESPONSE_OK) dialog.set_markup(_('Currently Uploading')) dialog.format_secondary_text(_('Photos are still being uploaded. ' 'Are you sure you want to quit?')) response = dialog.run() dialog.destroy() if response == gtk.RESPONSE_CANCEL: return True elif self.is_connected and self.model.iter_n_children(None) > 0: dialog = gtk.MessageDialog(type=gtk.MESSAGE_WARNING, parent=self.window) dialog.add_buttons(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_QUIT, gtk.RESPONSE_OK) dialog.set_markup(_('Photos to be uploaded')) dialog.format_secondary_text(_('There are photos pending to ' 'be uploaded. ' 'Are you sure you want to quit?')) response = dialog.run() dialog.destroy() if response == gtk.RESPONSE_CANCEL: return True import twisted.internet.reactor twisted.internet.reactor.stop() def on_remove_activate(self, widget): """Callback from File->Remove or Remove button.""" selection = self.thumbview.get_selection() (model, items) = selection.get_selected_rows() # Remove the items for path in items: self.model.remove(self.model.get_iter(path)) # Select a new row try: self.thumbview.set_cursor(self.model[items[0]].path) except IndexError: # TODO: It appears that the ability to simply do # gtk_tree_path_previous() is missing in PyGTK. path = list(items[-1]) if path[0]: path[0] -= 1 self.thumbview.set_cursor(self.model[tuple(path)].path) self.update_statusbar() def on_select_all_activate(self, menuitem): """Callback from Edit->Select All.""" selection = self.thumbview.get_selection() selection.select_all() def on_deselect_all_activate(self, menuitem): """Callback from Edit->Deselect All.""" selection = self.thumbview.get_selection() selection.unselect_all() def on_invert_selection_activate(self, menuitem): """Callback from Edit->Invert Selection.""" selection = self.thumbview.get_selection() selected = selection.get_selected_rows()[1] for row in self.model: if row.path in selected: selection.unselect_iter(row.iter) else: selection.select_iter(row.iter) def on_switch_activate(self, menuitem): """Callback from File->Switch User.""" self.flickr.clear_cached() self.flickr.authenticate_1().addCallbacks(self.auth_open_url, self.twisted_error) def on_upload_activate(self, menuitem): """Callback from File->Upload.""" if self.uploading: print "Upload should be disabled, currently uploading" return it = self.model.get_iter_first() if it is None: print "Upload should be disabled, no photos" return self.upload_menu.set_sensitive(False) self.upload_button.set_sensitive(False) self.uploading = True self.thumbview.set_sensitive(False) self.progress_dialog.show() self.upload_count = self.model.iter_n_children (None) self.upload_index = 0 self.upload() def on_about_activate(self, menuitem): """Callback from Help->About.""" dialog = AboutDialog() dialog.set_transient_for(self.window) dialog.run() dialog.destroy() def update_thumbnail(self, widget, allocation = None): """Update the preview, as the selected image was changed.""" if self.current_it: if not allocation: allocation = widget.get_allocation() force = True else: force = False # hrngh. seemingly a size-allocate call (with identical params, # mind) gets called every time we call set_from_pixbuf. even if # we connect it to the window. so very braindead. if not force and self.old_thumb_allocation and \ self.old_thumb_allocation.width == allocation.width and \ self.old_thumb_allocation.height == allocation.height: return; self.old_thumb_allocation = allocation (simage,) = self.model.get(self.current_it, ImageStore.COL_PREVIEW) tw = allocation.width th = allocation.height # Clamp the size to 512 if tw > 512: tw = 512 if th > 512: th = 512 (tw, th) = get_thumb_size(simage.get_width(), simage.get_height(), tw, th) thumb = simage.scale_simple(tw, th, gtk.gdk.INTERP_BILINEAR) widget.set_from_pixbuf(thumb) def on_selection_changed(self, selection): """Callback when the selection was changed, to update the entries and preview.""" [obj.handler_block(i) for obj,i in self.change_signals] def enable_field(field, value): field.set_sensitive(True) if isinstance(field, gtk.Entry): field.set_text(value) elif isinstance(field, gtk.TextView): field.get_buffer().set_text (value) elif isinstance(field, gtk.ToggleButton): field.set_active(value) elif isinstance(field, gtk.ComboBox): if value: field.set_active_iter(value) else: # This means the default value is always the first field.set_active(0) elif isinstance(field, GroupSelector.GroupSelector): field.set_selected_groups(value) else: raise "Unhandled widget type %s" % field def disable_field(field): field.set_sensitive(False) if isinstance(field, gtk.Entry): field.set_text("") elif isinstance(field, gtk.TextView): field.get_buffer().set_text ("") elif isinstance(field, gtk.ToggleButton): field.set_active(True) elif isinstance(field, gtk.ComboBox): field.set_active(-1) elif isinstance(field, GroupSelector.GroupSelector): field.set_selected_groups(()) else: raise "Unhandled widget type %s" % field (model, items) = selection.get_selected_rows() if items: # TODO: do something clever with multiple selections self.current_it = self.model.get_iter(items[0]) (title, desc, tags, set_it, groups, privacy_it, safety_it, visible) = self.model.get(self.current_it, ImageStore.COL_TITLE, ImageStore.COL_DESCRIPTION, ImageStore.COL_TAGS, ImageStore.COL_SET, ImageStore.COL_GROUPS, ImageStore.COL_PRIVACY, ImageStore.COL_SAFETY, ImageStore.COL_VISIBLE) enable_field(self.title_entry, title) enable_field(self.desc_view, desc) enable_field(self.tags_entry, tags) enable_field(self.set_combo, set_it) enable_field(self.group_selector, groups) enable_field(self.privacy_combo, privacy_it) enable_field(self.safety_combo, safety_it) enable_field(self.visible_check, visible) self.update_thumbnail(self.thumbnail_image) else: self.current_it = None disable_field(self.title_entry) disable_field(self.desc_view) disable_field(self.tags_entry) disable_field(self.set_combo) disable_field(self.group_selector) disable_field(self.privacy_combo) disable_field(self.safety_combo) disable_field(self.visible_check) self.thumbnail_image.set_from_pixbuf(None) [obj.handler_unblock(i) for obj,i in self.change_signals] def add_image_filename(self, filename): """Add a file to the image list. Called by the File->Add Photo and drag and drop callbacks.""" # TODO: MIME type check # Check the file size try: filesize = os.path.getsize(filename) except os.error: d = ErrorDialog(self.window) d.set_from_string("File at %s does not exist or is currently inaccessible." % filename) d.show_all() return if filesize > 20 * 1024 * 1024: d = ErrorDialog(self.window) d.set_from_string("Image %s is too large, images must be no larger than 20MB in size." % filename) d.show_all() return # TODO: we open the file three times now, which is madness, especially # if gnome-vfs is used to read remote files. Need to find/write EXIF # and IPTC parsers that are incremental. # First we load the image scaled to 512x512 for the preview. try: preview = gtk.gdk.pixbuf_new_from_file_at_size(filename, 512, 512) except Exception, e: d = ErrorDialog(self.window) d.set_from_exception(e) d.show_all() return # On a file that doesn't contain EXIF, like a PNG, this just returns an # empty set. try: exif = EXIF.process_file(open(filename, 'rb')) except: exif = {} try: iptc = IPTCInfo(open(filename, 'rb')).data except: iptc = {} # Rotate the preview if required. We don't need to manipulate the # original data as Flickr will do that for us. if "Image Orientation" in exif: rotation = exif["Image Orientation"].values[0] if rotation == ROTATED_180: preview = preview.rotate_simple(gtk.gdk.PIXBUF_ROTATE_UPSIDEDOWN) elif rotation == ROTATED_90_CW: preview = preview.rotate_simple(gtk.gdk.PIXBUF_ROTATE_CLOCKWISE) elif rotation == ROTATED_90_CCW: preview = preview.rotate_simple(gtk.gdk.PIXBUF_ROTATE_COUNTERCLOCKWISE) # Now scale the preview to a thumbnail sizes = get_thumb_size(preview.get_width(), preview.get_height(), 64, 64) thumb = preview.scale_simple(sizes[0], sizes[1], gtk.gdk.INTERP_BILINEAR) # Slurp data from the EXIF and IPTC tags title_tags = ( (iptc, "headline"), ) desc_tags = ( (exif, "Image ImageDescription"), (exif, "UserComment"), (iptc, "caption/abstract"), ) tag_tags = ( (iptc, "keywords"), ) def slurp(tags, default=""): for (data, tag) in tags: if data.has_key(tag): value = data[tag] if isinstance (value, list): return ' '.join(map (lambda s: '"' + s + '"', value)) elif not isinstance (value, str): value = str(value) if value: return value return default title = slurp(title_tags, os.path.splitext(os.path.basename(filename))[0]) desc = slurp(desc_tags) tags = slurp(tag_tags) self.model.set(self.model.append(), ImageStore.COL_FILENAME, filename, ImageStore.COL_SIZE, filesize, ImageStore.COL_IMAGE, None, ImageStore.COL_PREVIEW, preview, ImageStore.COL_THUMBNAIL, thumb, ImageStore.COL_TITLE, title, ImageStore.COL_DESCRIPTION, desc, ImageStore.COL_TAGS, tags, ImageStore.COL_VISIBLE, True) self.update_statusbar() self.update_upload() def on_drag_data_received(self, widget, context, x, y, selection, targetType, timestamp): """Drag and drop callback when data is received.""" if targetType == ImageList.DRAG_IMAGE: pixbuf = selection.get_pixbuf() # TODO: don't scale up if the image is smaller than 512/512 # Scale the pixbuf to a preview sizes = get_thumb_size (pixbuf.get_width(), pixbuf.get_height(), 512, 512) preview = pixbuf.scale_simple(sizes[0], sizes[1], gtk.gdk.INTERP_BILINEAR) # Now scale to a thumbnail sizes = get_thumb_size (pixbuf.get_width(), pixbuf.get_height(), 64, 64) thumb = pixbuf.scale_simple(sizes[0], sizes[1], gtk.gdk.INTERP_BILINEAR) # TODO: This is wrong, and should generate a PNG here and use the # size of the PNG size = pixbuf.get_width() * pixbuf.get_height() * pixbuf.get_n_channels() self.model.set(self.model.append(), ImageStore.COL_IMAGE, pixbuf, ImageStore.COL_SIZE, size, ImageStore.COL_FILENAME, None, ImageStore.COL_PREVIEW, preview, ImageStore.COL_THUMBNAIL, thumb, ImageStore.COL_TITLE, "", ImageStore.COL_DESCRIPTION, "", ImageStore.COL_TAGS, "", ImageStore.COL_VISIBLE, True) elif targetType == ImageList.DRAG_URI: for uri in selection.get_uris(): # TODO: use gnome-vfs to handle remote files filename = urllib.unquote(urlparse(uri)[2]) if os.path.isfile(filename): self.add_image_filename(filename) elif os.path.isdir(filename): for root, dirs, files in os.walk(filename): for f in files: # TODO: handle symlinks to directories as they are # in files self.add_image_filename (os.path.join(root, f)) else: print "Unhandled file %s" % filename else: print "Unhandled target type %d" % targetType self.update_statusbar() context.finish(True, True, timestamp) def update_progress(self, title, filename, thumb): """Update the progress bar whilst uploading.""" label = '%s\n%s' % (title, basename(filename)) self.progress_dialog.label.set_label(label) try: self.progress_dialog.thumbnail.set_from_pixbuf(thumb) self.progress_dialog.thumbnail.show() except: self.progress_dialog.thumbnail.set_from_pixbuf(None) self.progress_dialog.thumbnail.hide() self.progress_dialog.progress.set_fraction(float(self.upload_index) / float(self.upload_count)) # Use named args for i18n data = { "index": self.upload_index+1, "count": self.upload_count } progress_label = _('Uploading %(index)d of %(count)d') % data self.progress_dialog.label.set_text(progress_label) self.window.set_title(_('Flickr Uploader (%(index)d/%(count)d)') % data) def add_to_set(self, rsp, set): """Callback from the upload method to add the picture to a set.""" photo_id=rsp.find("photoid").text self.flickr.photosets_addPhoto(photo_id=photo_id, photoset_id=set).addErrback(self.twisted_error) return rsp def add_to_groups(self, rsp, groups): """Callback from the upload method to add the picture to a groups.""" photo_id=rsp.find("photoid").text for group in groups: def error(failure): # Code 6 means "moderated", which isn't an error if failure.value.code != 6: twisted_error(self, failure) self.flickr.groups_pools_add(photo_id=photo_id, group_id=group).addErrback(error) return rsp def upload_done(self): self.cancel_upload = False self.window.set_title(_("Flickr Uploader")) self.upload_menu.set_sensitive(True) self.upload_button.set_sensitive(True) self.uploading = False self.progress_dialog.hide() self.thumbview.set_sensitive(True) self.update_statusbar() self.statusbar.update_quota() self.current_upload_it = None def upload_error(self, failure): self.twisted_error(failure) self.upload_done() def upload(self, response=None): """Upload worker function, called by the File->Upload callback. As this calls itself in the deferred callback, it takes a response argument.""" # Remove the uploaded image from the store if self.current_upload_it: self.model.remove(self.current_upload_it) self.current_upload_it = None it = self.model.get_iter_first() if self.cancel_upload or it is None: self.upload_done() return (filename, thumb, pixbuf, title, desc, tags, set_it, groups, privacy_it, safety_it, visible) = self.model.get(it, ImageStore.COL_FILENAME, ImageStore.COL_THUMBNAIL, ImageStore.COL_IMAGE, ImageStore.COL_TITLE, ImageStore.COL_DESCRIPTION, ImageStore.COL_TAGS, ImageStore.COL_SET, ImageStore.COL_GROUPS, ImageStore.COL_PRIVACY, ImageStore.COL_SAFETY, ImageStore.COL_VISIBLE) # Lookup the set ID from the iterator if set_it: (set_id,) = self.sets.get (set_it, 0) else: set_id = 0 if privacy_it: (is_public, is_family, is_friend) = self.privacy_combo.get_acls_for_iter(privacy_it) else: is_public = is_family = is_friend = None if safety_it: safety = self.safety_combo.get_safety_for_iter(safety_it) else: safety = None self.update_progress(filename, title, thumb) self.upload_index += 1 self.current_upload_it = it if filename: d = self.flickr.upload(filename=filename, title=title, desc=desc, tags=tags, search_hidden=not visible, safety=safety, is_public=is_public, is_family=is_family, is_friend=is_friend) elif pixbuf: # This isn't very nice, but might be the best way data = [] pixbuf.save_to_callback(lambda d: data.append(d), "png", {}) d = self.flickr.upload(imageData=''.join(data), title=title, desc=desc, tags=tags, search_hidden=not visible, safety=safety, is_public=is_public, is_family=is_family, is_friend=is_friend) else: print "No filename or pixbuf stored" if set_id: d.addCallback(self.add_to_set, set_id) if groups: d.addCallback(self.add_to_groups, groups) d.addCallbacks(self.upload, self.upload_error) postr-0.12.4/src/util.py0000644000175000017500000000756511274363367013606 0ustar gpoogpoo# Postr, a Flickr Uploader # # Copyright (C) 2006-2008 Ross Burton # # 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 2, 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, write to the Free Software Foundation, Inc., 51 Franklin # St, Fifth Floor, Boston, MA 02110-1301 USA import gtk, os def greek(size): """Take a quantity (like 1873627) and display it in a human-readable rounded form (like 1.8M)""" _abbrevs = [ (1<<50L, 'P'), (1<<40L, 'T'), (1<<30L, 'G'), (1<<20L, 'M'), (1<<10L, 'k'), (1, '') ] for factor, suffix in _abbrevs: if size > factor: break return "%.1f%s" % (float(size)/factor, suffix) def get_widget_checked(glade, name): """Get widget name from glade, and if it doesn't exist raise an exception instead of returning None.""" widget = glade.get_widget(name) if widget is None: raise "Cannot find widget %s" % name return widget def get_glade_widgets (glade, object, widget_names): """Get the widgets in the list widget_names from the GladeXML object glade and set them as attributes on object.""" for name in widget_names: setattr(object, name, get_widget_checked(glade, name)) def get_thumb_size(srcw, srch, dstw, dsth): """Scale scrw x srch to an dimensions with the same ratio that fits as closely as possible to dstw x dsth.""" scalew = dstw/float(srcw) scaleh = dsth/float(srch) scale = min(scalew, scaleh) return (int(srcw * scale), int(srch * scale)) def align_labels(glade, names): """Add the list of widgets identified by names in glade to a horizontal sizegroup.""" group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) widget = [group.add_widget(get_widget_checked(glade, name)) for name in names] __buddy_cache = None def get_buddyicon(flickr, data, size=48): """Lookup the buddyicon from the data in @data using @flickr and resize it to @size pixels.""" from twisted.web.client import getPage import dbhash, bsddb global __buddy_cache if __buddy_cache is None: folder = os.path.join (get_cache_path(), "postr") if not os.path.exists(folder): os.makedirs(folder) path = os.path.join (folder, "buddyicons") try: __buddy_cache = dbhash.open(path, "c") except bsddb.db.DBInvalidArgError: # The database needs upgrading, so delete it os.remove(path) __buddy_cache = dbhash.open(path, "c") def load_thumb(page, size): loader = gtk.gdk.PixbufLoader() loader.set_size (size, size) loader.write(page) loader.close() return loader.get_pixbuf() def got_data(page, url, size): __buddy_cache[url] = page return load_thumb(page, size) if int(data.get("iconfarm")) > 0: url = "http://farm%s.static.flickr.com/%s/buddyicons/%s.jpg" % (data.get("iconfarm"), data.get("iconserver"), data.get("nsid")) else: url = "http://www.flickr.com/images/buddyicon.jpg" if __buddy_cache.has_key(url): from twisted.internet import defer return defer.succeed(load_thumb(__buddy_cache[url], size)) else: return getPage(url).addCallback(got_data, url, size) def get_cache_path(): """Return the location of the XDG cache directory.""" return os.environ.get("XDG_CACHE_HOME", os.path.expanduser("~/.cache/")) postr-0.12.4/src/DummyUnique.py0000644000175000017500000000173711274363367015106 0ustar gpoogpoo# Postr, a Flickr Uploader # # Copyright (C) 2006-2008 Ross Burton # # 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 2, 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, write to the Free Software Foundation, Inc., 51 Franklin # St, Fifth Floor, Boston, MA 02110-1301 USA class UniqueApp: """A dummy UniqueApp for when gtkunique isn't installed.""" def __init__(self, name): pass def add_window(self, window): pass def is_running(self): return False postr-0.12.4/src/ImageList.py0000644000175000017500000000550511274424736014475 0ustar gpoogpoo# Postr, a Flickr Uploader # # Copyright (C) 2006-2008 Ross Burton # # 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 2, 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, write to the Free Software Foundation, Inc., 51 Franklin # St, Fifth Floor, Boston, MA 02110-1301 USA import gtk import pango import ImageStore # Constants for the drag handling (DRAG_URI, DRAG_IMAGE) = range (0, 2) class ImageList(gtk.TreeView): def __init__(self): gtk.TreeView.__init__(self) column = gtk.TreeViewColumn('Preview', gtk.CellRendererPixbuf(), pixbuf=ImageStore.COL_THUMBNAIL) self.append_column(column) renderer = gtk.CellRendererText() renderer.set_property('ellipsize', pango.ELLIPSIZE_END) column = gtk.TreeViewColumn('Info', renderer) column.set_cell_data_func(renderer, self.data_func) self.append_column(column) self.set_headers_visible(False) self.set_enable_search(False) selection = self.get_selection() selection.set_mode(gtk.SELECTION_MULTIPLE) # Setup the drag and drop self.drag_dest_set (gtk.DEST_DEFAULT_ALL, (), gtk.gdk.ACTION_COPY) targets = self.drag_dest_get_target_list() targets = gtk.target_list_add_image_targets (targets, DRAG_IMAGE, False) targets = gtk.target_list_add_uri_targets (targets, DRAG_URI) self.drag_dest_set_target_list (targets) def data_func(self, column, cell, model, it): from xml.sax.saxutils import escape (title, description, tags) = model.get(it, ImageStore.COL_TITLE, ImageStore.COL_DESCRIPTION, ImageStore.COL_TAGS) if title: info_title = title else: info_title = _("No title") if description: # Clip the description because it could be long and have multiple lines # TODO: Clip at 20 characters, or the first line. info_desc = description[:20] else: info_desc = _("No description") s = "%s\n%s\n" % (escape (info_title), escape (info_desc)) if tags: colour = self.style.text[gtk.STATE_INSENSITIVE].pixel s = s + "%s" % (colour, escape (tags)) cell.set_property("markup", s) postr-0.12.4/src/__init__.py0000644000175000017500000000000011274363367014341 0ustar gpoogpoopostr-0.12.4/src/AuthenticationDialog.py0000644000175000017500000000531511274363367016717 0ustar gpoogpoo# Postr, a Flickr Uploader # # Copyright (C) 2006-2008 Ross Burton # # 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 2, 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, write to the Free Software Foundation, Inc., 51 Franklin # St, Fifth Floor, Boston, MA 02110-1301 USA import os, gtk, gconf def on_url_clicked(button, url): """Global LinkButton handler that starts the default GNOME HTTP handler, or firefox.""" # Get the HTTP URL handler client = gconf.client_get_default() browser = client.get_string("/desktop/gnome/url-handlers/http/command") or "firefox" # Because the world sucks and everyone hates me, just use the first word and # hope that is enough. The problem is that some people have [epiphany %s] # which means the & needs escaping or quoting, others have [iceweasel # -remote "openurl(%s,newtab)"] which means the & must not be escaped or # quoted. I can't see a general solution browser = browser.split(" ")[0] os.spawnlp(os.P_NOWAIT, browser, browser, url) # TODO: if that didn't work fallback on x-www-browser or something class AuthenticationDialog(gtk.Dialog): def __init__(self, parent, url): gtk.Dialog.__init__(self, title=_("Flickr Uploader"), parent=parent, flags=gtk.DIALOG_NO_SEPARATOR, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, _("Continue"), gtk.RESPONSE_ACCEPT)) vbox = gtk.VBox(spacing=8) vbox.set_border_width(8) label = gtk.Label(_("Postr needs to login to Flickr to upload your photos. " "Please click on the link below to login to Flickr.")) label.set_line_wrap(True) vbox.add(label) # gtk.LinkButton is only in 2.10, so use a normal button if it isn't # available. if hasattr(gtk, "LinkButton"): gtk.link_button_set_uri_hook(on_url_clicked) button = gtk.LinkButton(url, _("Login to Flickr")) else: button = gtk.Button(_("Login to Flickr")) button.connect("clicked", on_url_clicked, url) vbox.add(button) self.vbox.add(vbox) self.show_all() postr-0.12.4/src/ImageStore.py0000644000175000017500000000436611274363367014664 0ustar gpoogpoo# Postr, a Flickr Uploader # # Copyright (C) 2006-2008 Ross Burton # # 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 2, 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, write to the Free Software Foundation, Inc., 51 Franklin # St, Fifth Floor, Boston, MA 02110-1301 USA import gobject, gtk # Column indexes (COL_FILENAME, # The filename of an image (can be None) COL_SIZE, # Integer, file size COL_IMAGE, # The image data (if filename is None) COL_PREVIEW, # A 512x512 preview of the image COL_THUMBNAIL, # A 64x64 thumbnail of the image COL_TITLE, # The image title COL_DESCRIPTION, # The image description COL_TAGS, # A space deliminated list of tags for the image COL_SET, # An iterator point to the set to put the photo in COL_GROUPS, # Pyton list of group IDs COL_PRIVACY, # Iterator containing privacy rules COL_SAFETY, # Iterator containing safety COL_VISIBLE # If the image is searchable ) = range (0, 13) class ImageStore (gtk.ListStore): def __init__(self): gtk.ListStore.__init__(self, gobject.TYPE_STRING, # COL_FILENAME gobject.TYPE_INT, # COL_SIZE gtk.gdk.Pixbuf, # COL_IMAGE gtk.gdk.Pixbuf, # COL_PREVIEW gtk.gdk.Pixbuf, #COL_THUMBNAIL gobject.TYPE_STRING, # COL_TITLE gobject.TYPE_STRING, # COL_DESCRIPTION gobject.TYPE_STRING, # COL_TAGS gtk.TreeIter, # COL_SET object, # COL_GROUPS gtk.TreeIter, # COL_PRIVACY gtk.TreeIter, # COL_SAFETY gobject.TYPE_BOOLEAN) # COL_VISIBLE postr-0.12.4/src/proxyclient.py0000644000175000017500000003341711274363367015204 0ustar gpoogpoo# -*- test-case-name: twisted.web.test.test_webclient -*- # Copyright (c) 2001-2004 Twisted Matrix Laboratories. # See LICENSE for details. # """HTTP client. API Stability: stable """ import urlparse, os, types from twisted.web import http from twisted.internet import defer, protocol, reactor from twisted.python import failure from twisted.python.util import InsensitiveDict from twisted.web import error class PartialDownloadError(error.Error): """Page was only partially downloaded, we got disconnected in middle. The bit that was downloaded is in the response attribute. """ class HTTPPageGetter(http.HTTPClient): quietLoss = 0 followRedirect = 1 failed = 0 def connectionMade(self): method = getattr(self.factory, 'method', 'GET') self.sendCommand(method, self.factory.path) self.sendHeader('Host', self.factory.headers.get("host", self.factory.host)) self.sendHeader('User-Agent', self.factory.agent) if self.factory.cookies: l=[] for cookie, cookval in self.factory.cookies.items(): l.append('%s=%s' % (cookie, cookval)) self.sendHeader('Cookie', '; '.join(l)) data = getattr(self.factory, 'postdata', None) if data is not None: self.sendHeader("Content-Length", str(len(data))) for (key, value) in self.factory.headers.items(): if key.lower() != "content-length": # we calculated it on our own self.sendHeader(key, value) self.endHeaders() self.headers = {} if data is not None: self.transport.write(data) def handleHeader(self, key, value): key = key.lower() l = self.headers[key] = self.headers.get(key, []) l.append(value) def handleStatus(self, version, status, message): self.version, self.status, self.message = version, status, message self.factory.gotStatus(version, status, message) def handleEndHeaders(self): self.factory.gotHeaders(self.headers) m = getattr(self, 'handleStatus_'+self.status, self.handleStatusDefault) m() def handleStatus_200(self): pass handleStatus_201 = lambda self: self.handleStatus_200() handleStatus_202 = lambda self: self.handleStatus_200() def handleStatusDefault(self): self.failed = 1 def handleStatus_301(self): l = self.headers.get('location') if not l: self.handleStatusDefault() return url = l[0] if self.followRedirect: scheme, host, port, path = \ _parse(url, defaultPort=self.transport.getPeer().port) self.factory.setURL(url) if self.factory.scheme == 'https': from twisted.internet import ssl contextFactory = ssl.ClientContextFactory() reactor.connectSSL(self.factory.host, self.factory.port, self.factory, contextFactory) else: reactor.connectTCP(self.factory.host, self.factory.port, self.factory) else: self.handleStatusDefault() self.factory.noPage( failure.Failure( error.PageRedirect( self.status, self.message, location = url))) self.quietLoss = 1 self.transport.loseConnection() handleStatus_302 = lambda self: self.handleStatus_301() def handleStatus_303(self): self.factory.method = 'GET' self.handleStatus_301() def connectionLost(self, reason): if not self.quietLoss: http.HTTPClient.connectionLost(self, reason) self.factory.noPage(reason) def handleResponse(self, response): if self.quietLoss: return if self.failed: self.factory.noPage( failure.Failure( error.Error( self.status, self.message, response))) elif self.length != None and self.length != 0: self.factory.noPage(failure.Failure( PartialDownloadError(self.status, self.message, response))) else: self.factory.page(response) # server might be stupid and not close connection. admittedly # the fact we do only one request per connection is also # stupid... self.transport.loseConnection() def timeout(self): self.quietLoss = True self.transport.loseConnection() self.factory.noPage(defer.TimeoutError("Getting %s took longer than %s seconds." % (self.factory.url, self.factory.timeout))) class HTTPPageDownloader(HTTPPageGetter): transmittingPage = 0 def handleStatus_200(self, partialContent=0): HTTPPageGetter.handleStatus_200(self) self.transmittingPage = 1 self.factory.pageStart(partialContent) def handleStatus_206(self): self.handleStatus_200(partialContent=1) def handleResponsePart(self, data): if self.transmittingPage: self.factory.pagePart(data) def handleResponseEnd(self): if self.transmittingPage: self.factory.pageEnd() self.transmittingPage = 0 if self.failed: self.factory.noPage( failure.Failure( error.Error( self.status, self.message, None))) self.transport.loseConnection() class HTTPClientFactory(protocol.ClientFactory): """Download a given URL. @type deferred: Deferred @ivar deferred: A Deferred that will fire when the content has been retrieved. Once this is fired, the ivars `status', `version', and `message' will be set. @type status: str @ivar status: The status of the response. @type version: str @ivar version: The version of the response. @type message: str @ivar message: The text message returned with the status. @type response_headers: dict @ivar response_headers: The headers that were specified in the response from the server. """ protocol = HTTPPageGetter url = None scheme = None host = '' port = None path = None def __init__(self, url, method='GET', postdata=None, headers=None, agent="Twisted PageGetter", timeout=0, cookies=None, followRedirect=1, proxy=None): self.protocol.followRedirect = followRedirect self.timeout = timeout self.agent = agent self.proxy = proxy if cookies is None: cookies = {} self.cookies = cookies if headers is not None: self.headers = InsensitiveDict(headers) else: self.headers = InsensitiveDict() if postdata is not None: self.headers.setdefault('Content-Length', len(postdata)) # just in case a broken http/1.1 decides to keep connection alive self.headers.setdefault("connection", "close") self.postdata = postdata self.method = method self.setURL(url) self.waiting = 1 self.deferred = defer.Deferred() self.response_headers = None def __repr__(self): return "<%s: %s>" % (self.__class__.__name__, self.url) def setURL(self, url): self.url = url scheme, host, port, path = _parse(url) if scheme and host: self.scheme = scheme self.host = host self.port = port if self.proxy: self.path = "%s://%s:%s%s" % (self.scheme, self.host, self.port, path) else: self.path = path def buildProtocol(self, addr): p = protocol.ClientFactory.buildProtocol(self, addr) if self.timeout: timeoutCall = reactor.callLater(self.timeout, p.timeout) self.deferred.addBoth(self._cancelTimeout, timeoutCall) return p def _cancelTimeout(self, result, timeoutCall): if timeoutCall.active(): timeoutCall.cancel() return result def gotHeaders(self, headers): self.response_headers = headers if headers.has_key('set-cookie'): for cookie in headers['set-cookie']: cookparts = cookie.split(';') cook = cookparts[0] cook.lstrip() k, v = cook.split('=', 1) self.cookies[k.lstrip()] = v.lstrip() def gotStatus(self, version, status, message): self.version, self.status, self.message = version, status, message def page(self, page): if self.waiting: self.waiting = 0 self.deferred.callback(page) def noPage(self, reason): if self.waiting: self.waiting = 0 self.deferred.errback(reason) def clientConnectionFailed(self, _, reason): if self.waiting: self.waiting = 0 self.deferred.errback(reason) class HTTPDownloader(HTTPClientFactory): """Download to a file.""" protocol = HTTPPageDownloader value = None def __init__(self, url, fileOrName, method='GET', postdata=None, headers=None, agent="Twisted client", supportPartial=0): self.requestedPartial = 0 if isinstance(fileOrName, types.StringTypes): self.fileName = fileOrName self.file = None if supportPartial and os.path.exists(self.fileName): fileLength = os.path.getsize(self.fileName) if fileLength: self.requestedPartial = fileLength if headers == None: headers = {} headers["range"] = "bytes=%d-" % fileLength else: self.file = fileOrName HTTPClientFactory.__init__(self, url, method=method, postdata=postdata, headers=headers, agent=agent) self.deferred = defer.Deferred() self.waiting = 1 def gotHeaders(self, headers): if self.requestedPartial: contentRange = headers.get("content-range", None) if not contentRange: # server doesn't support partial requests, oh well self.requestedPartial = 0 return start, end, realLength = http.parseContentRange(contentRange[0]) if start != self.requestedPartial: # server is acting wierdly self.requestedPartial = 0 def openFile(self, partialContent): if partialContent: file = open(self.fileName, 'rb+') file.seek(0, 2) else: file = open(self.fileName, 'wb') return file def pageStart(self, partialContent): """Called on page download start. @param partialContent: tells us if the download is partial download we requested. """ if partialContent and not self.requestedPartial: raise ValueError, "we shouldn't get partial content response if we didn't want it!" if self.waiting: self.waiting = 0 try: if not self.file: self.file = self.openFile(partialContent) except IOError: #raise self.deferred.errback(failure.Failure()) def pagePart(self, data): if not self.file: return try: self.file.write(data) except IOError: #raise self.file = None self.deferred.errback(failure.Failure()) def pageEnd(self): if not self.file: return try: self.file.close() except IOError: self.deferred.errback(failure.Failure()) return self.deferred.callback(self.value) def _parse(url, defaultPort=None): url = url.strip() parsed = urlparse.urlparse(url) scheme = parsed[0] path = urlparse.urlunparse(('','')+parsed[2:]) if defaultPort is None: if scheme == 'https': defaultPort = 443 else: defaultPort = 80 host, port = parsed[1], defaultPort if ':' in host: host, port = host.split(':') port = int(port) if path == "": path = "/" return scheme, host, port, path def getPage(url, contextFactory=None, proxy=None, *args, **kwargs): """Download a web page as a string. Download a page. Return a deferred, which will callback with a page (as a string) or errback with a description of the error. See HTTPClientFactory to see what extra args can be passed. """ if proxy: scheme, host, port, path = _parse(proxy) kwargs['proxy'] = proxy else: scheme, host, port, path = _parse(url) factory = HTTPClientFactory(url, *args, **kwargs) if scheme == 'https': from twisted.internet import ssl if contextFactory is None: contextFactory = ssl.ClientContextFactory() reactor.connectSSL(host, port, factory, contextFactory) else: reactor.connectTCP(host, port, factory) return factory.deferred def downloadPage(url, file, contextFactory=None, *args, **kwargs): """Download a web page to a file. @param file: path to file on filesystem, or file-like object. See HTTPDownloader to see what extra args can be passed. """ scheme, host, port, path = _parse(url) factory = HTTPDownloader(url, file, *args, **kwargs) if scheme == 'https': from twisted.internet import ssl if contextFactory is None: contextFactory = ssl.ClientContextFactory() reactor.connectSSL(host, port, factory, contextFactory) else: reactor.connectTCP(host, port, factory) return factory.deferred postr-0.12.4/src/flickrest.py0000644000175000017500000002536111274363367014611 0ustar gpoogpoo# flickrpc -- a Flickr client library. # # Copyright (C) 2007 Ross Burton # # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation; either version 2, 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 Lesser General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin # St, Fifth Floor, Boston, MA 02110-1301 USA import logging, md5, os, mimetools, urllib from twisted.internet import defer from twisted.python.failure import Failure import proxyclient as client try: from xml.etree import ElementTree except ImportError: from elementtree import ElementTree class FlickrError(Exception): def __init__(self, code, message): Exception.__init__(self) self.code = int(code) self.message = message def __str__(self): return "%d: %s" % (self.code, self.message) (SIZE_SQUARE, SIZE_THUMB, SIZE_SMALL, SIZE_MEDIUM, SIZE_LARGE) = range (0, 5) class Flickr: endpoint = "http://api.flickr.com/services/rest/?" def __init__(self, api_key, secret, perms="read"): self.__methods = {} self.api_key = api_key self.secret = secret self.perms = perms self.token = None self.logger = logging.getLogger('flickrest') self.set_proxy(os.environ.get("http_proxy", None)) self.fullname = None self.username = None self.nsid = None def get_fullname(self): return self.fullname def get_username(self): return self.username def get_nsid(self): return self.nsid def set_proxy(self, proxy): # Handle proxies which are not URLs if proxy and "://" not in proxy: proxy = "http://" + proxy self.proxy = proxy def __repr__(self): return "" def __getTokenFile(self): """Get the filename that contains the authentication token for the API key""" return os.path.expanduser(os.path.join("~", ".flickr", self.api_key, "auth.xml")) def clear_cached(self): """Remove any cached information on disk.""" self.fullname = None self.username = None self.nsid = None token = self.__getTokenFile() if os.path.exists(token): os.remove(token) self.token = None def __sign(self, kwargs): kwargs['api_key'] = self.api_key # If authenticating we don't yet have a token if self.token: kwargs['auth_token'] = self.token # I know this is less efficient than working with lists, but this is # much more readable. sig = reduce(lambda sig, key: sig + key + str(kwargs[key]), sorted(kwargs.keys()), self.secret) kwargs['api_sig'] = md5.new(sig).hexdigest() def __call(self, method, kwargs): kwargs["method"] = method self.__sign(kwargs) self.logger.info("Calling %s" % method) return client.getPage(Flickr.endpoint, proxy=self.proxy, method="POST", headers={"Content-Type": "application/x-www-form-urlencoded"}, postdata=urllib.urlencode(kwargs)) def __cb(self, data, method): self.logger.info("%s returned" % method) xml = ElementTree.XML(data) if xml.tag == "rsp" and xml.get("stat") == "ok": return xml elif xml.tag == "rsp" and xml.get("stat") == "fail": err = xml.find("err") raise FlickrError(err.get("code"), err.get("msg")) else: # Fake an error in this case raise FlickrError(0, "Invalid response") def __getattr__(self, method): method = "flickr." + method.replace("_", ".") if not self.__methods.has_key(method): def proxy(method=method, **kwargs): return self.__call(method, kwargs).addCallback(self.__cb, method) self.__methods[method] = proxy return self.__methods[method] @staticmethod def __encodeForm(inputs): """ Takes a dict of inputs and returns a multipart/form-data string containing the utf-8 encoded data. Keys must be strings, values can be either strings or file-like objects. """ boundary = mimetools.choose_boundary() lines = [] for key, val in inputs.items(): lines.append("--" + boundary.encode("utf-8")) header = 'Content-Disposition: form-data; name="%s";' % key # Objects with name value are probably files if hasattr(val, 'name'): header += 'filename="%s";' % os.path.split(val.name)[1] lines.append(header) header = "Content-Type: application/octet-stream" lines.append(header) lines.append("") # If there is a read attribute, it is a file-like object, so read all the data if hasattr(val, 'read'): lines.append(val.read()) # Otherwise just hope it is string-like and encode it to # UTF-8. TODO: this breaks when val is binary data. else: lines.append(str(val).encode('utf-8')) # Add final boundary. lines.append("--" + boundary.encode("utf-8")) return (boundary, '\r\n'.join(lines)) def upload(self, filename=None, imageData=None, title=None, desc=None, tags=None, is_public=None, is_family=None, is_friend=None, safety=None, search_hidden=None): # Sanity check the arguments if filename is None and imageData is None: raise ValueError("Need to pass either filename or imageData") if filename and imageData: raise ValueError("Cannot pass both filename and imageData") kwargs = {} if title: kwargs['title'] = title if desc: kwargs['description'] = desc if tags: kwargs['tags'] = tags if is_public is not None: kwargs['is_public'] = is_public and 1 or 0 if is_family is not None: kwargs['is_family'] = is_family and 1 or 0 if is_friend is not None: kwargs['is_friend'] = is_friend and 1 or 0 if safety: kwargs['safety_level'] = safety if search_hidden is not None: kwargs['hidden'] = search_hidden and 2 or 1 # Why Flickr, why? self.__sign(kwargs) self.logger.info("Upload args %s" % kwargs) if imageData: kwargs['photo'] = imageData else: kwargs['photo'] = file(filename, "rb") (boundary, form) = self.__encodeForm(kwargs) headers= { "Content-Type": "multipart/form-data; boundary=%s" % boundary, "Content-Length": str(len(form)) } self.logger.info("Calling upload") return client.getPage("http://api.flickr.com/services/upload/", proxy=self.proxy, method="POST", headers=headers, postdata=form).addCallback(self.__cb, "upload") def authenticate_2(self, state): def gotToken(e): # Set the token self.token = e.find("auth/token").text # Pulling out the user information user = e.find("auth/user") # Setting the user variables self.fullname = user.get("fullname") self.username = user.get("username") self.nsid = user.get("nsid") # Cache the authentication filename = self.__getTokenFile() path = os.path.dirname(filename) if not os.path.exists(path): os.makedirs(path, 0700) f = file(filename, "w") f.write(ElementTree.tostring(e)) f.close() # Callback to the user return True return self.auth_getToken(frob=state['frob']).addCallback(gotToken) def __get_frob(self): """Make the getFrob() call.""" def gotFrob(xml): frob = xml.find("frob").text keys = { 'perms': self.perms, 'frob': frob } self.__sign(keys) url = "http://flickr.com/services/auth/?api_key=%(api_key)s&perms=%(perms)s&frob=%(frob)s&api_sig=%(api_sig)s" % keys return {'url': url, 'frob': frob} return self.auth_getFrob().addCallback(gotFrob) def authenticate_1(self): """Attempts to log in to Flickr. The return value is a Twisted Deferred object that callbacks when the first part of the authentication is completed. If the result passed to the deferred callback is None, then the required authentication was locally cached and you are authenticated. Otherwise the result is a dictionary, you should open the URL specified by the 'url' key and instruct the user to follow the instructions. Once that is done, pass the state to flickrest.authenticate_2().""" filename = self.__getTokenFile() if os.path.exists(filename): try: e = ElementTree.parse(filename).getroot() self.token = e.find("auth/token").text user = e.find("auth/user") self.fullname = user.get("fullname") self.username = user.get("username") self.nsid = user.get("nsid") def reply(xml): return defer.succeed(None) def failed(failure): # If checkToken() failed, we need to re-authenticate self.clear_cached() return self.__get_frob() return self.auth_checkToken().addCallbacks(reply, failed) except: # TODO: print the exception to stderr? pass return self.__get_frob() @staticmethod def get_photo_url(photo, size=SIZE_MEDIUM): if photo is None: return None # Handle medium as the default suffix = "" if size == SIZE_SQUARE: suffix = "_s" elif size == SIZE_THUMB: suffix = "_t" elif size == SIZE_SMALL: suffix = "_m" elif size == SIZE_LARGE: suffix = "_b" return "http://static.flickr.com/%s/%s_%s%s.jpg" % (photo.get("server"), photo.get("id"), photo.get("secret"), suffix) postr-0.12.4/src/postr.glade0000644000175000017500000010737311274424736014420 0ustar gpoogpoo True Flickr Uploader 640 350 True True True _File True True _Add Photos... True True gtk-open 1 True _Remove Photos True True gtk-remove 1 True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK _Switch user... True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-refresh True _Upload True True gtk-connect 1 True True gtk-quit True True True Select True True _Select All True True gtk-select-all 1 True Dese_lect All True True _Invert Selection True True _Help True True gtk-about True True False False True True 200 True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True True GTK_POLICY_AUTOMATIC GTK_POLICY_AUTOMATIC GTK_SHADOW_IN True True True ImageList.ImageList() True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-add False True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-remove False 1 True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 2 6 True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-connect False True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK _Upload True upload_button 1 False GTK_PACK_END 2 False 1 False False True 6 7 3 4 6 True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK GTK_POLICY_AUTOMATIC GTK_POLICY_AUTOMATIC GTK_SHADOW_IN True False True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK GTK_WRAP_WORD False 1 3 2 3 GTK_FILL True 0 0 gtk-missing-image 3 True False 1 3 4 5 GTK_FILL GTK_FILL True 0 Add to _Set: True set_combo 4 5 GTK_FILL True True ... GTK_RELIEF_HALF True 0 2 3 3 4 GTK_FILL True False True 1 3 1 2 True False True 1 2 3 4 True 0 Ta_gs: True tags_entry 3 4 GTK_FILL True 0 0 _Description: True 2 3 GTK_FILL GTK_FILL True 0 _Title: True title_entry 1 2 GTK_FILL True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 3 2 4 6 True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK SafetyCombo.SafetyCombo() 1 2 1 2 True 0 _Safety: True tags_entry 1 2 GTK_FILL GTK_FILL True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK _Visible in search results True 0 True True 1 2 2 3 True 0 _Privacy: True tags_entry GTK_FILL GTK_FILL True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK PrivacyCombo.PrivacyCombo() 1 2 True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Privacy and Safety label_item 3 6 7 GTK_FILL GTK_FILL True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK GTK_POLICY_NEVER GTK_POLICY_AUTOMATIC GTK_SHADOW_IN True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK group_selector_new True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Send to Groups label_item 3 5 6 GTK_FILL GTK_FILL True True 1 True status_bar_new False False 2 postr-0.12.4/src/GroupSelector.py0000644000175000017500000000733511274424736015417 0ustar gpoogpoo# Postr, a Flickr Uploader # # Copyright (C) 2008 Ross Burton # # 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 2, 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, write to the Free Software Foundation, Inc., 51 Franklin # St, Fifth Floor, Boston, MA 02110-1301 USA import gobject, gtk, pango from ErrorDialog import ErrorDialog import util (COL_SELECTED, COL_ID, COL_NAME, COL_ICON) = range(0, 4) class GroupSelector(gtk.TreeView): __gsignals__ = { 'changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()) } def __init__(self, flickr): self.flickr = flickr self.model = gtk.ListStore(gobject.TYPE_BOOLEAN, gobject.TYPE_STRING, gobject.TYPE_STRING, gtk.gdk.Pixbuf) self.model.connect("row-changed", lambda model, path, iter: self.emit("changed")) gtk.TreeView.__init__(self, self.model) column = gtk.TreeViewColumn('Selected') self.append_column(column) renderer = gtk.CellRendererToggle() def toggled(r, path): self.model[path][COL_SELECTED] = not r.get_active() renderer.connect("toggled", toggled) column.pack_start(renderer, False) column.add_attribute(renderer, "active", COL_SELECTED) column = gtk.TreeViewColumn('Group') self.append_column(column) renderer = gtk.CellRendererPixbuf() column.pack_start(renderer, False) column.add_attribute(renderer, "pixbuf", COL_ICON) renderer = gtk.CellRendererText() column.pack_start(renderer, True) column.add_attribute(renderer, "text", COL_NAME) self.set_size_request(-1, 24 * 3 + self.style_get_property("vertical-separator") * 6) self.set_headers_visible(False) self.set_search_column(COL_NAME) def search_func(model, column, key, iter): s = model.get_value(iter, column) # This API is braindead, false=matches return key.lower() not in s.lower() self.set_search_equal_func(search_func) # TODO: enable case insensitive substring searching def update(self): # TODO: block changed signals self.flickr.groups_pools_getGroups().addCallbacks(self.got_groups, self.twisted_error) def got_groups(self, rsp): for group in rsp.findall("groups/group"): it = self.model.append() self.model.set (it, COL_ID, group.get("id"), COL_NAME, group.get("name")) def got_thumb(thumb, it): self.model.set (it, COL_ICON, thumb) util.get_buddyicon(self.flickr, group, 24).addCallback(got_thumb, it) def twisted_error(self, failure): dialog = ErrorDialog(self.window) dialog.set_from_failure(failure) dialog.show_all() def get_selected_groups(self): return [row[COL_ID] for row in self.model if row[COL_SELECTED]] def set_selected_groups(self, groups): # Handle groups being None */ if groups is None: groups = () for row in self.model: row[COL_SELECTED] = row[COL_ID] in groups postr-0.12.4/src/EXIF.py0000644000175000017500000012170111274363367013351 0ustar gpoogpoo# Library to extract EXIF information in digital camera image files # # To use this library call with: # f=open(path_name, 'rb') # tags=EXIF.process_file(f) # tags will now be a dictionary mapping names of EXIF tags to their # values in the file named by path_name. You can process the tags # as you wish. In particular, you can iterate through all the tags with: # for tag in tags.keys(): # if tag not in ('JPEGThumbnail', 'TIFFThumbnail', 'Filename', # 'EXIF MakerNote'): # print "Key: %s, value %s" % (tag, tags[tag]) # (This code uses the if statement to avoid printing out a few of the # tags that tend to be long or boring.) # # The tags dictionary will include keys for all of the usual EXIF # tags, and will also include keys for Makernotes used by some # cameras, for which we have a good specification. # # Contains code from "exifdump.py" originally written by Thierry Bousch # and released into the public domain. # # Updated and turned into general-purpose library by Gene Cash # # This copyright license is intended to be similar to the FreeBSD license. # # Copyright 2002 Gene Cash All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the # distribution. # # THIS SOFTWARE IS PROVIDED BY GENE CASH ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # # This means you may do anything you want with this code, except claim you # wrote it. Also, if it breaks you get to keep both pieces. # # Patch Contributors: # * Simon J. Gerraty # s2n fix & orientation decode # * John T. Riedl # Added support for newer Nikon type 3 Makernote format for D70 and some # other Nikon cameras. # * Joerg Schaefer # Fixed subtle bug when faking an EXIF header, which affected maker notes # using relative offsets, and a fix for Nikon D100. # # 21-AUG-99 TB Last update by Thierry Bousch to his code. # 17-JAN-02 CEC Discovered code on web. # Commented everything. # Made small code improvements. # Reformatted for readability. # 19-JAN-02 CEC Added ability to read TIFFs and JFIF-format JPEGs. # Added ability to extract JPEG formatted thumbnail. # Added ability to read GPS IFD (not tested). # Converted IFD data structure to dictionaries indexed by # tag name. # Factored into library returning dictionary of IFDs plus # thumbnail, if any. # 20-JAN-02 CEC Added MakerNote processing logic. # Added Olympus MakerNote. # Converted data structure to single-level dictionary, avoiding # tag name collisions by prefixing with IFD name. This makes # it much easier to use. # 23-JAN-02 CEC Trimmed nulls from end of string values. # 25-JAN-02 CEC Discovered JPEG thumbnail in Olympus TIFF MakerNote. # 26-JAN-02 CEC Added ability to extract TIFF thumbnails. # Added Nikon, Fujifilm, Casio MakerNotes. # 30-NOV-03 CEC Fixed problem with canon_decode_tag() not creating an # IFD_Tag() object. # 15-FEB-04 CEC Finally fixed bit shift warning by converting Y to 0L. # # field type descriptions as (length, abbreviation, full name) tuples FIELD_TYPES=( (0, 'X', 'Proprietary'), # no such type (1, 'B', 'Byte'), (1, 'A', 'ASCII'), (2, 'S', 'Short'), (4, 'L', 'Long'), (8, 'R', 'Ratio'), (1, 'SB', 'Signed Byte'), (1, 'U', 'Undefined'), (2, 'SS', 'Signed Short'), (4, 'SL', 'Signed Long'), (8, 'SR', 'Signed Ratio') ) # dictionary of main EXIF tag names # first element of tuple is tag name, optional second element is # another dictionary giving names to values EXIF_TAGS={ 0x0100: ('ImageWidth', ), 0x0101: ('ImageLength', ), 0x0102: ('BitsPerSample', ), 0x0103: ('Compression', {1: 'Uncompressed TIFF', 6: 'JPEG Compressed'}), 0x0106: ('PhotometricInterpretation', ), 0x010A: ('FillOrder', ), 0x010D: ('DocumentName', ), 0x010E: ('ImageDescription', ), 0x010F: ('Make', ), 0x0110: ('Model', ), 0x0111: ('StripOffsets', ), 0x0112: ('Orientation', {1: 'Horizontal (normal)', 2: 'Mirrored horizontal', 3: 'Rotated 180', 4: 'Mirrored vertical', 5: 'Mirrored horizontal then rotated 90 CCW', 6: 'Rotated 90 CW', 7: 'Mirrored horizontal then rotated 90 CW', 8: 'Rotated 90 CCW'}), 0x0115: ('SamplesPerPixel', ), 0x0116: ('RowsPerStrip', ), 0x0117: ('StripByteCounts', ), 0x011A: ('XResolution', ), 0x011B: ('YResolution', ), 0x011C: ('PlanarConfiguration', ), 0x0128: ('ResolutionUnit', {1: 'Not Absolute', 2: 'Pixels/Inch', 3: 'Pixels/Centimeter'}), 0x012D: ('TransferFunction', ), 0x0131: ('Software', ), 0x0132: ('DateTime', ), 0x013B: ('Artist', ), 0x013E: ('WhitePoint', ), 0x013F: ('PrimaryChromaticities', ), 0x0156: ('TransferRange', ), 0x0200: ('JPEGProc', ), 0x0201: ('JPEGInterchangeFormat', ), 0x0202: ('JPEGInterchangeFormatLength', ), 0x0211: ('YCbCrCoefficients', ), 0x0212: ('YCbCrSubSampling', ), 0x0213: ('YCbCrPositioning', ), 0x0214: ('ReferenceBlackWhite', ), 0x828D: ('CFARepeatPatternDim', ), 0x828E: ('CFAPattern', ), 0x828F: ('BatteryLevel', ), 0x8298: ('Copyright', ), 0x829A: ('ExposureTime', ), 0x829D: ('FNumber', ), 0x83BB: ('IPTC/NAA', ), 0x8769: ('ExifOffset', ), 0x8773: ('InterColorProfile', ), 0x8822: ('ExposureProgram', {0: 'Unidentified', 1: 'Manual', 2: 'Program Normal', 3: 'Aperture Priority', 4: 'Shutter Priority', 5: 'Program Creative', 6: 'Program Action', 7: 'Portrait Mode', 8: 'Landscape Mode'}), 0x8824: ('SpectralSensitivity', ), 0x8825: ('GPSInfo', ), 0x8827: ('ISOSpeedRatings', ), 0x8828: ('OECF', ), # print as string 0x9000: ('ExifVersion', lambda x: ''.join(map(chr, x))), 0x9003: ('DateTimeOriginal', ), 0x9004: ('DateTimeDigitized', ), 0x9101: ('ComponentsConfiguration', {0: '', 1: 'Y', 2: 'Cb', 3: 'Cr', 4: 'Red', 5: 'Green', 6: 'Blue'}), 0x9102: ('CompressedBitsPerPixel', ), 0x9201: ('ShutterSpeedValue', ), 0x9202: ('ApertureValue', ), 0x9203: ('BrightnessValue', ), 0x9204: ('ExposureBiasValue', ), 0x9205: ('MaxApertureValue', ), 0x9206: ('SubjectDistance', ), 0x9207: ('MeteringMode', {0: 'Unidentified', 1: 'Average', 2: 'CenterWeightedAverage', 3: 'Spot', 4: 'MultiSpot'}), 0x9208: ('LightSource', {0: 'Unknown', 1: 'Daylight', 2: 'Fluorescent', 3: 'Tungsten', 10: 'Flash', 17: 'Standard Light A', 18: 'Standard Light B', 19: 'Standard Light C', 20: 'D55', 21: 'D65', 22: 'D75', 255: 'Other'}), 0x9209: ('Flash', {0: 'No', 1: 'Fired', 5: 'Fired (?)', # no return sensed 7: 'Fired (!)', # return sensed 9: 'Fill Fired', 13: 'Fill Fired (?)', 15: 'Fill Fired (!)', 16: 'Off', 24: 'Auto Off', 25: 'Auto Fired', 29: 'Auto Fired (?)', 31: 'Auto Fired (!)', 32: 'Not Available'}), 0x920A: ('FocalLength', ), 0x927C: ('MakerNote', ), # print as string 0x9286: ('UserComment', lambda x: ''.join(map(chr, x))), 0x9290: ('SubSecTime', ), 0x9291: ('SubSecTimeOriginal', ), 0x9292: ('SubSecTimeDigitized', ), # print as string 0xA000: ('FlashPixVersion', lambda x: ''.join(map(chr, x))), 0xA001: ('ColorSpace', ), 0xA002: ('ExifImageWidth', ), 0xA003: ('ExifImageLength', ), 0xA005: ('InteroperabilityOffset', ), 0xA20B: ('FlashEnergy', ), # 0x920B in TIFF/EP 0xA20C: ('SpatialFrequencyResponse', ), # 0x920C - - 0xA20E: ('FocalPlaneXResolution', ), # 0x920E - - 0xA20F: ('FocalPlaneYResolution', ), # 0x920F - - 0xA210: ('FocalPlaneResolutionUnit', ), # 0x9210 - - 0xA214: ('SubjectLocation', ), # 0x9214 - - 0xA215: ('ExposureIndex', ), # 0x9215 - - 0xA217: ('SensingMethod', ), # 0x9217 - - 0xA300: ('FileSource', {3: 'Digital Camera'}), 0xA301: ('SceneType', {1: 'Directly Photographed'}), 0xA302: ('CVAPattern',), } # interoperability tags INTR_TAGS={ 0x0001: ('InteroperabilityIndex', ), 0x0002: ('InteroperabilityVersion', ), 0x1000: ('RelatedImageFileFormat', ), 0x1001: ('RelatedImageWidth', ), 0x1002: ('RelatedImageLength', ), } # GPS tags (not used yet, haven't seen camera with GPS) GPS_TAGS={ 0x0000: ('GPSVersionID', ), 0x0001: ('GPSLatitudeRef', ), 0x0002: ('GPSLatitude', ), 0x0003: ('GPSLongitudeRef', ), 0x0004: ('GPSLongitude', ), 0x0005: ('GPSAltitudeRef', ), 0x0006: ('GPSAltitude', ), 0x0007: ('GPSTimeStamp', ), 0x0008: ('GPSSatellites', ), 0x0009: ('GPSStatus', ), 0x000A: ('GPSMeasureMode', ), 0x000B: ('GPSDOP', ), 0x000C: ('GPSSpeedRef', ), 0x000D: ('GPSSpeed', ), 0x000E: ('GPSTrackRef', ), 0x000F: ('GPSTrack', ), 0x0010: ('GPSImgDirectionRef', ), 0x0011: ('GPSImgDirection', ), 0x0012: ('GPSMapDatum', ), 0x0013: ('GPSDestLatitudeRef', ), 0x0014: ('GPSDestLatitude', ), 0x0015: ('GPSDestLongitudeRef', ), 0x0016: ('GPSDestLongitude', ), 0x0017: ('GPSDestBearingRef', ), 0x0018: ('GPSDestBearing', ), 0x0019: ('GPSDestDistanceRef', ), 0x001A: ('GPSDestDistance', ) } # Nikon E99x MakerNote Tags # http://members.tripod.com/~tawba/990exif.htm MAKERNOTE_NIKON_NEWER_TAGS={ 0x0002: ('ISOSetting', ), 0x0003: ('ColorMode', ), 0x0004: ('Quality', ), 0x0005: ('Whitebalance', ), 0x0006: ('ImageSharpening', ), 0x0007: ('FocusMode', ), 0x0008: ('FlashSetting', ), 0x0009: ('AutoFlashMode', ), 0x000B: ('WhiteBalanceBias', ), 0x000C: ('WhiteBalanceRBCoeff', ), 0x000F: ('ISOSelection', ), 0x0012: ('FlashCompensation', ), 0x0013: ('ISOSpeedRequested', ), 0x0016: ('PhotoCornerCoordinates', ), 0x0018: ('FlashBracketCompensationApplied', ), 0x0019: ('AEBracketCompensationApplied', ), 0x0080: ('ImageAdjustment', ), 0x0081: ('ToneCompensation', ), 0x0082: ('AuxiliaryLens', ), 0x0083: ('LensType', ), 0x0084: ('LensMinMaxFocalMaxAperture', ), 0x0085: ('ManualFocusDistance', ), 0x0086: ('DigitalZoomFactor', ), 0x0088: ('AFFocusPosition', {0x0000: 'Center', 0x0100: 'Top', 0x0200: 'Bottom', 0x0300: 'Left', 0x0400: 'Right'}), 0x0089: ('BracketingMode', {0x00: 'Single frame, no bracketing', 0x01: 'Continuous, no bracketing', 0x02: 'Timer, no bracketing', 0x10: 'Single frame, exposure bracketing', 0x11: 'Continuous, exposure bracketing', 0x12: 'Timer, exposure bracketing', 0x40: 'Single frame, white balance bracketing', 0x41: 'Continuous, white balance bracketing', 0x42: 'Timer, white balance bracketing'}), 0x008D: ('ColorMode', ), 0x008F: ('SceneMode?', ), 0x0090: ('LightingType', ), 0x0092: ('HueAdjustment', ), 0x0094: ('Saturation', {-3: 'B&W', -2: '-2', -1: '-1', 0: '0', 1: '1', 2: '2'}), 0x0095: ('NoiseReduction', ), 0x00A7: ('TotalShutterReleases', ), 0x00A9: ('ImageOptimization', ), 0x00AA: ('Saturation', ), 0x00AB: ('DigitalVariProgram', ), 0x0010: ('DataDump', ) } MAKERNOTE_NIKON_OLDER_TAGS={ 0x0003: ('Quality', {1: 'VGA Basic', 2: 'VGA Normal', 3: 'VGA Fine', 4: 'SXGA Basic', 5: 'SXGA Normal', 6: 'SXGA Fine'}), 0x0004: ('ColorMode', {1: 'Color', 2: 'Monochrome'}), 0x0005: ('ImageAdjustment', {0: 'Normal', 1: 'Bright+', 2: 'Bright-', 3: 'Contrast+', 4: 'Contrast-'}), 0x0006: ('CCDSpeed', {0: 'ISO 80', 2: 'ISO 160', 4: 'ISO 320', 5: 'ISO 100'}), 0x0007: ('WhiteBalance', {0: 'Auto', 1: 'Preset', 2: 'Daylight', 3: 'Incandescent', 4: 'Fluorescent', 5: 'Cloudy', 6: 'Speed Light'}) } # decode Olympus SpecialMode tag in MakerNote def olympus_special_mode(v): a={ 0: 'Normal', 1: 'Unknown', 2: 'Fast', 3: 'Panorama'} b={ 0: 'Non-panoramic', 1: 'Left to right', 2: 'Right to left', 3: 'Bottom to top', 4: 'Top to bottom'} return '%s - sequence %d - %s' % (a[v[0]], v[1], b[v[2]]) MAKERNOTE_OLYMPUS_TAGS={ # ah HAH! those sneeeeeaky bastids! this is how they get past the fact # that a JPEG thumbnail is not allowed in an uncompressed TIFF file 0x0100: ('JPEGThumbnail', ), 0x0200: ('SpecialMode', olympus_special_mode), 0x0201: ('JPEGQual', {1: 'SQ', 2: 'HQ', 3: 'SHQ'}), 0x0202: ('Macro', {0: 'Normal', 1: 'Macro'}), 0x0204: ('DigitalZoom', ), 0x0207: ('SoftwareRelease', ), 0x0208: ('PictureInfo', ), # print as string 0x0209: ('CameraID', lambda x: ''.join(map(chr, x))), 0x0F00: ('DataDump', ) } MAKERNOTE_CASIO_TAGS={ 0x0001: ('RecordingMode', {1: 'Single Shutter', 2: 'Panorama', 3: 'Night Scene', 4: 'Portrait', 5: 'Landscape'}), 0x0002: ('Quality', {1: 'Economy', 2: 'Normal', 3: 'Fine'}), 0x0003: ('FocusingMode', {2: 'Macro', 3: 'Auto Focus', 4: 'Manual Focus', 5: 'Infinity'}), 0x0004: ('FlashMode', {1: 'Auto', 2: 'On', 3: 'Off', 4: 'Red Eye Reduction'}), 0x0005: ('FlashIntensity', {11: 'Weak', 13: 'Normal', 15: 'Strong'}), 0x0006: ('Object Distance', ), 0x0007: ('WhiteBalance', {1: 'Auto', 2: 'Tungsten', 3: 'Daylight', 4: 'Fluorescent', 5: 'Shade', 129: 'Manual'}), 0x000B: ('Sharpness', {0: 'Normal', 1: 'Soft', 2: 'Hard'}), 0x000C: ('Contrast', {0: 'Normal', 1: 'Low', 2: 'High'}), 0x000D: ('Saturation', {0: 'Normal', 1: 'Low', 2: 'High'}), 0x0014: ('CCDSpeed', {64: 'Normal', 80: 'Normal', 100: 'High', 125: '+1.0', 244: '+3.0', 250: '+2.0',}) } MAKERNOTE_FUJIFILM_TAGS={ 0x0000: ('NoteVersion', lambda x: ''.join(map(chr, x))), 0x1000: ('Quality', ), 0x1001: ('Sharpness', {1: 'Soft', 2: 'Soft', 3: 'Normal', 4: 'Hard', 5: 'Hard'}), 0x1002: ('WhiteBalance', {0: 'Auto', 256: 'Daylight', 512: 'Cloudy', 768: 'DaylightColor-Fluorescent', 769: 'DaywhiteColor-Fluorescent', 770: 'White-Fluorescent', 1024: 'Incandescent', 3840: 'Custom'}), 0x1003: ('Color', {0: 'Normal', 256: 'High', 512: 'Low'}), 0x1004: ('Tone', {0: 'Normal', 256: 'High', 512: 'Low'}), 0x1010: ('FlashMode', {0: 'Auto', 1: 'On', 2: 'Off', 3: 'Red Eye Reduction'}), 0x1011: ('FlashStrength', ), 0x1020: ('Macro', {0: 'Off', 1: 'On'}), 0x1021: ('FocusMode', {0: 'Auto', 1: 'Manual'}), 0x1030: ('SlowSync', {0: 'Off', 1: 'On'}), 0x1031: ('PictureMode', {0: 'Auto', 1: 'Portrait', 2: 'Landscape', 4: 'Sports', 5: 'Night', 6: 'Program AE', 256: 'Aperture Priority AE', 512: 'Shutter Priority AE', 768: 'Manual Exposure'}), 0x1100: ('MotorOrBracket', {0: 'Off', 1: 'On'}), 0x1300: ('BlurWarning', {0: 'Off', 1: 'On'}), 0x1301: ('FocusWarning', {0: 'Off', 1: 'On'}), 0x1302: ('AEWarning', {0: 'Off', 1: 'On'}) } MAKERNOTE_CANON_TAGS={ 0x0006: ('ImageType', ), 0x0007: ('FirmwareVersion', ), 0x0008: ('ImageNumber', ), 0x0009: ('OwnerName', ) } # see http://www.burren.cx/david/canon.html by David Burren # this is in element offset, name, optional value dictionary format MAKERNOTE_CANON_TAG_0x001={ 1: ('Macromode', {1: 'Macro', 2: 'Normal'}), 2: ('SelfTimer', ), 3: ('Quality', {2: 'Normal', 3: 'Fine', 5: 'Superfine'}), 4: ('FlashMode', {0: 'Flash Not Fired', 1: 'Auto', 2: 'On', 3: 'Red-Eye Reduction', 4: 'Slow Synchro', 5: 'Auto + Red-Eye Reduction', 6: 'On + Red-Eye Reduction', 16: 'external flash'}), 5: ('ContinuousDriveMode', {0: 'Single Or Timer', 1: 'Continuous'}), 7: ('FocusMode', {0: 'One-Shot', 1: 'AI Servo', 2: 'AI Focus', 3: 'MF', 4: 'Single', 5: 'Continuous', 6: 'MF'}), 10: ('ImageSize', {0: 'Large', 1: 'Medium', 2: 'Small'}), 11: ('EasyShootingMode', {0: 'Full Auto', 1: 'Manual', 2: 'Landscape', 3: 'Fast Shutter', 4: 'Slow Shutter', 5: 'Night', 6: 'B&W', 7: 'Sepia', 8: 'Portrait', 9: 'Sports', 10: 'Macro/Close-Up', 11: 'Pan Focus'}), 12: ('DigitalZoom', {0: 'None', 1: '2x', 2: '4x'}), 13: ('Contrast', {0xFFFF: 'Low', 0: 'Normal', 1: 'High'}), 14: ('Saturation', {0xFFFF: 'Low', 0: 'Normal', 1: 'High'}), 15: ('Sharpness', {0xFFFF: 'Low', 0: 'Normal', 1: 'High'}), 16: ('ISO', {0: 'See ISOSpeedRatings Tag', 15: 'Auto', 16: '50', 17: '100', 18: '200', 19: '400'}), 17: ('MeteringMode', {3: 'Evaluative', 4: 'Partial', 5: 'Center-weighted'}), 18: ('FocusType', {0: 'Manual', 1: 'Auto', 3: 'Close-Up (Macro)', 8: 'Locked (Pan Mode)'}), 19: ('AFPointSelected', {0x3000: 'None (MF)', 0x3001: 'Auto-Selected', 0x3002: 'Right', 0x3003: 'Center', 0x3004: 'Left'}), 20: ('ExposureMode', {0: 'Easy Shooting', 1: 'Program', 2: 'Tv-priority', 3: 'Av-priority', 4: 'Manual', 5: 'A-DEP'}), 23: ('LongFocalLengthOfLensInFocalUnits', ), 24: ('ShortFocalLengthOfLensInFocalUnits', ), 25: ('FocalUnitsPerMM', ), 28: ('FlashActivity', {0: 'Did Not Fire', 1: 'Fired'}), 29: ('FlashDetails', {14: 'External E-TTL', 13: 'Internal Flash', 11: 'FP Sync Used', 7: '2nd("Rear")-Curtain Sync Used', 4: 'FP Sync Enabled'}), 32: ('FocusMode', {0: 'Single', 1: 'Continuous'}) } MAKERNOTE_CANON_TAG_0x004={ 7: ('WhiteBalance', {0: 'Auto', 1: 'Sunny', 2: 'Cloudy', 3: 'Tungsten', 4: 'Fluorescent', 5: 'Flash', 6: 'Custom'}), 9: ('SequenceNumber', ), 14: ('AFPointUsed', ), 15: ('FlashBias', {0XFFC0: '-2 EV', 0XFFCC: '-1.67 EV', 0XFFD0: '-1.50 EV', 0XFFD4: '-1.33 EV', 0XFFE0: '-1 EV', 0XFFEC: '-0.67 EV', 0XFFF0: '-0.50 EV', 0XFFF4: '-0.33 EV', 0X0000: '0 EV', 0X000C: '0.33 EV', 0X0010: '0.50 EV', 0X0014: '0.67 EV', 0X0020: '1 EV', 0X002C: '1.33 EV', 0X0030: '1.50 EV', 0X0034: '1.67 EV', 0X0040: '2 EV'}), 19: ('SubjectDistance', ) } # extract multibyte integer in Motorola format (little endian) def s2n_motorola(str): x=0 for c in str: x=(x << 8) | ord(c) return x # extract multibyte integer in Intel format (big endian) def s2n_intel(str): x=0 y=0L for c in str: x=x | (ord(c) << y) y=y+8 return x # ratio object that eventually will be able to reduce itself to lowest # common denominator for printing def gcd(a, b): if b == 0: return a else: return gcd(b, a % b) class Ratio: def __init__(self, num, den): self.num=num self.den=den def __repr__(self): self.reduce() if self.den == 1: return str(self.num) return '%d/%d' % (self.num, self.den) def reduce(self): div=gcd(self.num, self.den) if div > 1: self.num=self.num/div self.den=self.den/div # for ease of dealing with tags class IFD_Tag: def __init__(self, printable, tag, field_type, values, field_offset, field_length): # printable version of data self.printable=printable # tag ID number self.tag=tag # field type as index into FIELD_TYPES self.field_type=field_type # offset of start of field in bytes from beginning of IFD self.field_offset=field_offset # length of data field in bytes self.field_length=field_length # either a string or array of data items self.values=values def __str__(self): return self.printable def __repr__(self): return '(0x%04X) %s=%s @ %d' % (self.tag, FIELD_TYPES[self.field_type][2], self.printable, self.field_offset) # class that handles an EXIF header class EXIF_header: def __init__(self, file, endian, offset, fake_exif, debug=0): self.file=file self.endian=endian self.offset=offset self.fake_exif=fake_exif self.debug=debug self.tags={} # convert slice to integer, based on sign and endian flags # usually this offset is assumed to be relative to the beginning of the # start of the EXIF information. For some cameras that use relative tags, # this offset may be relative to some other starting point. def s2n(self, offset, length, signed=0): self.file.seek(self.offset+offset) slice=self.file.read(length) if self.endian == 'I': val=s2n_intel(slice) else: val=s2n_motorola(slice) # Sign extension ? if signed: msb=1L << (8*length-1) if val & msb: val=val-(msb << 1) return val # convert offset to string def n2s(self, offset, length): s='' for i in range(length): if self.endian == 'I': s=s+chr(offset & 0xFF) else: s=chr(offset & 0xFF)+s offset=offset >> 8 return s # return first IFD def first_IFD(self): return self.s2n(4, 4) # return pointer to next IFD def next_IFD(self, ifd): entries=self.s2n(ifd, 2) return self.s2n(ifd+2+12*entries, 4) # return list of IFDs in header def list_IFDs(self): i=self.first_IFD() a=[] while i: a.append(i) i=self.next_IFD(i) return a # return list of entries in this IFD def dump_IFD(self, ifd, ifd_name, dict=EXIF_TAGS, relative=0): entries=self.s2n(ifd, 2) for i in range(entries): # entry is index of start of this IFD in the file entry=ifd+2+12*i tag=self.s2n(entry, 2) # get tag name. We do it early to make debugging easier tag_entry=dict.get(tag) if tag_entry: tag_name=tag_entry[0] else: tag_name='Tag 0x%04X' % tag field_type=self.s2n(entry+2, 2) if not 0 < field_type < len(FIELD_TYPES): # unknown field type raise ValueError, \ 'unknown type %d in tag 0x%04X' % (field_type, tag) typelen=FIELD_TYPES[field_type][0] count=self.s2n(entry+4, 4) offset=entry+8 if count*typelen > 4: # offset is not the value; it's a pointer to the value # if relative we set things up so s2n will seek to the right # place when it adds self.offset. Note that this 'relative' # is for the Nikon type 3 makernote. Other cameras may use # other relative offsets, which would have to be computed here # slightly differently. if relative: tmp_offset=self.s2n(offset, 4) offset=tmp_offset+ifd-self.offset+4 if self.fake_exif: offset=offset+18 else: offset=self.s2n(offset, 4) field_offset=offset if field_type == 2: # special case: null-terminated ASCII string if count != 0: self.file.seek(self.offset+offset) values=self.file.read(count) values=values.strip().replace('\x00','') else: values='' else: values=[] signed=(field_type in [6, 8, 9, 10]) for j in range(count): if field_type in (5, 10): # a ratio value_j=Ratio(self.s2n(offset, 4, signed), self.s2n(offset+4, 4, signed)) else: value_j=self.s2n(offset, typelen, signed) values.append(value_j) offset=offset+typelen # now "values" is either a string or an array if count == 1 and field_type != 2: printable=str(values[0]) else: printable=str(values) # compute printable version of values if tag_entry: if len(tag_entry) != 1: # optional 2nd tag element is present if callable(tag_entry[1]): # call mapping function printable=tag_entry[1](values) else: printable='' for i in values: # use lookup table for this tag printable+=tag_entry[1].get(i, repr(i)) self.tags[ifd_name+' '+tag_name]=IFD_Tag(printable, tag, field_type, values, field_offset, count*typelen) if self.debug: print ' debug: %s: %s' % (tag_name, repr(self.tags[ifd_name+' '+tag_name])) # extract uncompressed TIFF thumbnail (like pulling teeth) # we take advantage of the pre-existing layout in the thumbnail IFD as # much as possible def extract_TIFF_thumbnail(self, thumb_ifd): entries=self.s2n(thumb_ifd, 2) # this is header plus offset to IFD ... if self.endian == 'M': tiff='MM\x00*\x00\x00\x00\x08' else: tiff='II*\x00\x08\x00\x00\x00' # ... plus thumbnail IFD data plus a null "next IFD" pointer self.file.seek(self.offset+thumb_ifd) tiff+=self.file.read(entries*12+2)+'\x00\x00\x00\x00' # fix up large value offset pointers into data area for i in range(entries): entry=thumb_ifd+2+12*i tag=self.s2n(entry, 2) field_type=self.s2n(entry+2, 2) typelen=FIELD_TYPES[field_type][0] count=self.s2n(entry+4, 4) oldoff=self.s2n(entry+8, 4) # start of the 4-byte pointer area in entry ptr=i*12+18 # remember strip offsets location if tag == 0x0111: strip_off=ptr strip_len=count*typelen # is it in the data area? if count*typelen > 4: # update offset pointer (nasty "strings are immutable" crap) # should be able to say "tiff[ptr:ptr+4]=newoff" newoff=len(tiff) tiff=tiff[:ptr]+self.n2s(newoff, 4)+tiff[ptr+4:] # remember strip offsets location if tag == 0x0111: strip_off=newoff strip_len=4 # get original data and store it self.file.seek(self.offset+oldoff) tiff+=self.file.read(count*typelen) # add pixel strips and update strip offset info old_offsets=self.tags['Thumbnail StripOffsets'].values old_counts=self.tags['Thumbnail StripByteCounts'].values for i in range(len(old_offsets)): # update offset pointer (more nasty "strings are immutable" crap) offset=self.n2s(len(tiff), strip_len) tiff=tiff[:strip_off]+offset+tiff[strip_off+strip_len:] strip_off+=strip_len # add pixel strip to end self.file.seek(self.offset+old_offsets[i]) tiff+=self.file.read(old_counts[i]) self.tags['TIFFThumbnail']=tiff # decode all the camera-specific MakerNote formats # Note is the data that comprises this MakerNote. The MakerNote will # likely have pointers in it that point to other parts of the file. We'll # use self.offset as the starting point for most of those pointers, since # they are relative to the beginning of the file. # # If the MakerNote is in a newer format, it may use relative addressing # within the MakerNote. In that case we'll use relative addresses for the # pointers. # # As an aside: it's not just to be annoying that the manufacturers use # relative offsets. It's so that if the makernote has to be moved by the # picture software all of the offsets don't have to be adjusted. Overall, # this is probably the right strategy for makernotes, though the spec is # ambiguous. (The spec does not appear to imagine that makernotes would # follow EXIF format internally. Once they did, it's ambiguous whether # the offsets should be from the header at the start of all the EXIF info, # or from the header at the start of the makernote.) def decode_maker_note(self): note=self.tags['EXIF MakerNote'] make=self.tags['Image Make'].printable model=self.tags['Image Model'].printable # Nikon # The maker note usually starts with the word Nikon, followed by the # type of the makernote (1 or 2, as a short). If the word Nikon is # not at the start of the makernote, it's probably type 2, since some # cameras work that way. if make in ('NIKON', 'NIKON CORPORATION'): if note.values[0:7] == [78, 105, 107, 111, 110, 00, 01]: if self.debug: print "Looks like a type 1 Nikon MakerNote." self.dump_IFD(note.field_offset+8, 'MakerNote', dict=MAKERNOTE_NIKON_OLDER_TAGS) elif note.values[0:7] == [78, 105, 107, 111, 110, 00, 02]: if self.debug: print "Looks like a labeled type 2 Nikon MakerNote" if note.values[12:14] != [0, 42] and note.values[12:14] != [42L, 0L]: raise ValueError, "Missing marker tag '42' in MakerNote." # skip the Makernote label and the TIFF header self.dump_IFD(note.field_offset+10+8, 'MakerNote', dict=MAKERNOTE_NIKON_NEWER_TAGS, relative=1) else: # E99x or D1 if self.debug: print "Looks like an unlabeled type 2 Nikon MakerNote" self.dump_IFD(note.field_offset, 'MakerNote', dict=MAKERNOTE_NIKON_NEWER_TAGS) return # Olympus if make[:7] == 'OLYMPUS': self.dump_IFD(note.field_offset+8, 'MakerNote', dict=MAKERNOTE_OLYMPUS_TAGS) return # Casio if make == 'Casio': self.dump_IFD(note.field_offset, 'MakerNote', dict=MAKERNOTE_CASIO_TAGS) return # Fujifilm if make == 'FUJIFILM': # bug: everything else is "Motorola" endian, but the MakerNote # is "Intel" endian endian=self.endian self.endian='I' # bug: IFD offsets are from beginning of MakerNote, not # beginning of file header offset=self.offset self.offset+=note.field_offset # process note with bogus values (note is actually at offset 12) self.dump_IFD(12, 'MakerNote', dict=MAKERNOTE_FUJIFILM_TAGS) # reset to correct values self.endian=endian self.offset=offset return # Canon if make == 'Canon': self.dump_IFD(note.field_offset, 'MakerNote', dict=MAKERNOTE_CANON_TAGS) for i in (('MakerNote Tag 0x0001', MAKERNOTE_CANON_TAG_0x001), ('MakerNote Tag 0x0004', MAKERNOTE_CANON_TAG_0x004)): self.canon_decode_tag(self.tags[i[0]].values, i[1]) return # decode Canon MakerNote tag based on offset within tag # see http://www.burren.cx/david/canon.html by David Burren def canon_decode_tag(self, value, dict): for i in range(1, len(value)): x=dict.get(i, ('Unknown', )) if self.debug: print i, x name=x[0] if len(x) > 1: val=x[1].get(value[i], 'Unknown') else: val=value[i] # it's not a real IFD Tag but we fake one to make everybody # happy. this will have a "proprietary" type self.tags['MakerNote '+name]=IFD_Tag(str(val), None, 0, None, None, None) # process an image file (expects an open file object) # this is the function that has to deal with all the arbitrary nasty bits # of the EXIF standard def process_file(file, debug=0): # determine whether it's a JPEG or TIFF data=file.read(12) if data[0:4] in ['II*\x00', 'MM\x00*']: # it's a TIFF file file.seek(0) endian=file.read(1) file.read(1) offset=0 elif data[0:2] == '\xFF\xD8': # it's a JPEG file # skip JFIF style header(s) fake_exif=0 while data[2] == '\xFF' and data[6:10] in ('JFIF', 'JFXX', 'OLYM'): length=ord(data[4])*256+ord(data[5]) file.read(length-8) # fake an EXIF beginning of file data='\xFF\x00'+file.read(10) fake_exif=1 if data[2] == '\xFF' and data[6:10] == 'Exif': # detected EXIF header offset=file.tell() endian=file.read(1) else: # no EXIF information return {} else: # file format not recognized return {} # deal with the EXIF info we found if debug: print {'I': 'Intel', 'M': 'Motorola'}[endian], 'format' hdr=EXIF_header(file, endian, offset, fake_exif, debug) ifd_list=hdr.list_IFDs() ctr=0 for i in ifd_list: if ctr == 0: IFD_name='Image' elif ctr == 1: IFD_name='Thumbnail' thumb_ifd=i else: IFD_name='IFD %d' % ctr if debug: print ' IFD %d (%s) at offset %d:' % (ctr, IFD_name, i) hdr.dump_IFD(i, IFD_name) # EXIF IFD exif_off=hdr.tags.get(IFD_name+' ExifOffset') if exif_off: if debug: print ' EXIF SubIFD at offset %d:' % exif_off.values[0] hdr.dump_IFD(exif_off.values[0], 'EXIF') # Interoperability IFD contained in EXIF IFD intr_off=hdr.tags.get('EXIF SubIFD InteroperabilityOffset') if intr_off: if debug: print ' EXIF Interoperability SubSubIFD at offset %d:' \ % intr_off.values[0] hdr.dump_IFD(intr_off.values[0], 'EXIF Interoperability', dict=INTR_TAGS) # GPS IFD gps_off=hdr.tags.get(IFD_name+' GPSInfo') if gps_off: if debug: print ' GPS SubIFD at offset %d:' % gps_off.values[0] hdr.dump_IFD(gps_off.values[0], 'GPS', dict=GPS_TAGS) ctr+=1 # extract uncompressed TIFF thumbnail thumb=hdr.tags.get('Thumbnail Compression') if thumb and thumb.printable == 'Uncompressed TIFF': hdr.extract_TIFF_thumbnail(thumb_ifd) # JPEG thumbnail (thankfully the JPEG data is stored as a unit) thumb_off=hdr.tags.get('Thumbnail JPEGInterchangeFormat') if thumb_off: file.seek(offset+thumb_off.values[0]) size=hdr.tags['Thumbnail JPEGInterchangeFormatLength'].values[0] hdr.tags['JPEGThumbnail']=file.read(size) # deal with MakerNote contained in EXIF IFD if hdr.tags.has_key('EXIF MakerNote'): hdr.decode_maker_note() # Sometimes in a TIFF file, a JPEG thumbnail is hidden in the MakerNote # since it's not allowed in a uncompressed TIFF IFD if not hdr.tags.has_key('JPEGThumbnail'): thumb_off=hdr.tags.get('MakerNote JPEGThumbnail') if thumb_off: file.seek(offset+thumb_off.values[0]) hdr.tags['JPEGThumbnail']=file.read(thumb_off.field_length) return hdr.tags # library test/debug function (dump given files) if __name__ == '__main__': import sys if len(sys.argv) < 2: print 'Usage: %s files...\n' % sys.argv[0] sys.exit(0) for filename in sys.argv[1:]: try: file=open(filename, 'rb') except: print filename, 'unreadable' print continue print filename+':' # data=process_file(file, 1) # with debug info data=process_file(file) if not data: print 'No EXIF information found' continue x=data.keys() x.sort() for i in x: if i in ('JPEGThumbnail', 'TIFFThumbnail'): continue try: print ' %s (%s): %s' % \ (i, FIELD_TYPES[data[i].field_type][2], data[i].printable) except: print 'error', i, '"', data[i], '"' if data.has_key('JPEGThumbnail'): print 'File has JPEG thumbnail' print postr-0.12.4/README0000644000175000017500000000015511274363367012334 0ustar gpoogpooFlickr Uploader aka Postr Copyright (C) 2006-2007 Ross Burton Postr requires PyGTK 2. postr-0.12.4/nautilus/0000755000175000017500000000000011274363367013317 5ustar gpoogpoopostr-0.12.4/nautilus/postrExtension.py0000644000175000017500000000601711274363367016741 0ustar gpoogpoo# Postr's Nautilus Extension, an extension to upload to Flickr using Postr # # Copyright (C) 2007 German Poo-Caaman~o # # 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 2, 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, write to the Free Software Foundation, Inc., 51 Franklin # St, Fifth Floor, Boston, MA 02110-1301 USA import gettext gettext.install('postr') import gobject, nautilus import os, os.path from urllib import unquote PROGRAM_NAME = 'postr' class PostrExtension(nautilus.MenuProvider): def __init__(self): # The constructor must be exists, even if there is nothing # to initialize (See Bug #374958) #self.program = None pass def locate_program(self, program_name): path_list = os.environ['PATH'] for d in path_list.split(os.path.pathsep): try: if program_name in os.listdir(d): return os.path.sep.join([d, program_name]) except OSError: # Normally is a bad idea use 'pass' in a exception, # but in this case we don't care if the directory # in path exists or not. pass return None def upload_files(self, menu, files): # This is the method invoked when our extension is activated # Do whatever you want to do with the files selected if len(files) == 0: return names = [ unquote(file.get_uri()[7:]) for file in files ] argv = [ PROGRAM_NAME ] + names # TODO: use startup notification gobject.spawn_async(argv, flags=gobject.SPAWN_SEARCH_PATH) def get_file_items(self, window, files): # Show the menu iif: # - There is at least on file selected # - All the selected files are images # - All selected images are locals (currently Postr doesn't have # support for gnome-vfs # - Postr is installed (is in PATH) if len(files) == 0: return for file in files: if file.is_directory() or file.get_uri_scheme() != 'file': return if not file.is_mime_type("image/*"): return #self.program = self.locate_program(PROGRAM_NAME) #if not self.program: # return item = nautilus.MenuItem('PostrExtension::upload_files', _('Upload to Flickr...'), _('Upload the selected files into Flickr')) item.connect('activate', self.upload_files, files) return item, postr-0.12.4/TODO0000644000175000017500000000110311274363367012136 0ustar gpoogpooMissing Features == Upload progress bar needs work: - Cancel - A real progress bar instead of activity Let user add photos to multiple sets and groups. Custom combo, when popped down shows textual list of selected items, when opened shows a menu with check boxes. Set privacy options When dropping image data store the data not a pixbuf in the model to allow dragging of lossy data Show date/time taken Nice interface for tagging CLI interface: postr -tags foobar -description blaa -title fooo img.jpg Resize before uploading Add option to clear authentication cookies postr-0.12.4/setup.py0000644000175000017500000000243611274424736013170 0ustar gpoogpoo#!/usr/bin/env python from distutils.core import setup from glob import glob from src.version import __version__ setup(name='Postr', version=__version__, description='Flickr Uploader', author='Ross Burton', author_email='ross@burtonini.com', url='http://www.burtonini.com/', scripts=['postr'], package_dir={'postr': 'src'}, packages=['postr'], package_data={'postr': ['postr.glade']}, data_files=[('share/applications', ['data/postr.desktop']), ('share/icons/hicolor/16x16/apps', glob('data/16x16/*.png')), ('share/icons/hicolor/22x22/apps', glob('data/22x22/*.png')), ('share/icons/hicolor/24x24/apps', glob('data/24x24/*.png')), ('share/icons/hicolor/32x32/apps', glob('data/32x32/*.png')), ('share/icons/hicolor/scalable/apps', glob('data/scalable/*.svg')), # TODO: inspect nautilus-python.pc to get path ('lib/nautilus/extensions-1.0/python', ['nautilus/postrExtension.py']), ('lib/nautilus/extensions-2.0/python', ['nautilus/postrExtension.py']), ], ) # TODO: install translations # TODO: update icon cache # TODO: rewrite in autotools because this is getting silly postr-0.12.4/postr.doap0000644000175000017500000001130411274424721013456 0ustar gpoogpoo Postr postr 2006-08-15 A simple Flickr uploader for GNOME. A simple Flickr uploader for GNOME. Python Linux Ross Burton Germán Póo-Caamaño gpoo 0.12.4 postr-0-12 0.12.4 2009-11-04 0.12.3 2008-12-19 0.12.2 2008-06-15 0.12.1 2008-05-27 0.12 2008-04-23 0.11 2008-04-20 0.10 2008-01-04 0.9 2007-09-23 0.8 2007-08-21 0.7 2007-06-13 0.6 2007-06-05 0.5 2007-02-09 0.4 2007-01-21 0.3 2006-12-16 0.2 2006-12-05 0.1 2006-10-01 postr-0.12.4/po/0000755000175000017500000000000011274424736012067 5ustar gpoogpoopostr-0.12.4/po/sv.po0000644000175000017500000000565411274363367013073 0ustar gpoogpoo# Swedish translation of Postr. # Copyright (C) 2007 Free Software Foundation, Inc. # This file is distributed under the same license as the postr package. # Daniel Nylander , 2007. # msgid "" msgstr "" "Project-Id-Version: Postr\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2007-01-10 10:04+0000\n" "PO-Revision-Date: 2007-06-09 14:08+0100\n" "Last-Translator: Daniel Nylander \n" "Language-Team: Swedish \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: src/AboutDialog.py:7 #: src/AuthenticationDialog.py:16 #: src/postr.glade:8 #: src/postr.glade:486 msgid "Flickr Uploader" msgstr "Skicka upp till Flickr" #: src/AuthenticationDialog.py:19 msgid "Continue" msgstr "Fortsätt" #: src/AuthenticationDialog.py:23 msgid "Postr needs to login to Flickr to upload your photos. Please click on the link below to login to Flickr." msgstr "Postr behöver logga in på Flickr för att skicka upp dina foton. Klicka på nedanstående länk för att logga in på Flickr." #: src/AuthenticationDialog.py:32 #: src/AuthenticationDialog.py:34 msgid "Login to Flickr" msgstr "Logga in på Flickr" #: src/postr.py:182 #, python-format msgid "You have %s remaining this month" msgstr "Du har %s återstående denna månad" #: src/postr.py:194 msgid "Add Photos" msgstr "Lägg till foton" #: src/postr.py:206 msgid "Images" msgstr "Bilder" #: src/postr.py:212 msgid "All Files" msgstr "Alla filer" #: src/postr.py:245 msgid "Currently Uploading" msgstr "Skickar upp" #: src/postr.py:246 msgid "Photos are still being uploaded. Are you sure you want to quit?" msgstr "Foton håller på att skickas. Är du säker på att du vill avsluta?" #: src/postr.py:509 #, python-format msgid "Uploading %(index)d of %(count)d" msgstr "Skickar upp %(index)d av %(count)d" #: nautilus/postrExtension.py:82 msgid "Upload to Flickr..." msgstr "Skicka upp till Flickr..." #: nautilus/postrExtension.py:83 msgid "Upload the selected files into Flickr" msgstr "Skicka upp markerade filer till Flickr" #: src/postr.glade:40 msgid "_File" msgstr "_Arkiv" #: src/postr.glade:49 msgid "_Add Photos..." msgstr "_Lägg till foton..." #: src/postr.glade:77 msgid "_Upload" msgstr "Skicka _upp" #: src/postr.glade:117 msgid "Select" msgstr "Markera" #: src/postr.glade:127 msgid "_Delete" msgstr "_Ta bort" #: src/postr.glade:155 msgid "_Select All" msgstr "_Markera allt" #: src/postr.glade:177 msgid "Dese_lect All" msgstr "Avmark_era allt" #: src/postr.glade:187 msgid "_Invert Selection" msgstr "_Invertera markering" #: src/postr.glade:200 msgid "_Help" msgstr "_Hjälp" #: src/postr.glade:209 msgid "_About" msgstr "_Om" #: src/postr.glade:272 msgid "_Title:" msgstr "_Titel:" #: src/postr.glade:301 msgid "_Description:" msgstr "_Beskrivning:" #: src/postr.glade:330 msgid "Ta_gs:" msgstr "Ta_ggar:" #: src/postr.glade:442 msgid "..." msgstr "..." postr-0.12.4/po/de.po0000644000175000017500000000563211274363367013027 0ustar gpoogpoo# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: Postr\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2007-01-10 10:04+0000\n" "PO-Revision-Date: 2007-04-06 20:53+0100\n" "Last-Translator: Vinzenz Vietzke \n" "Language-Team: German \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: src/AboutDialog.py:7 src/AuthenticationDialog.py:16 src/postr.glade:8 #: src/postr.glade:486 msgid "Flickr Uploader" msgstr "" #: src/AuthenticationDialog.py:19 msgid "Continue" msgstr "Fortsetzen" #: src/AuthenticationDialog.py:23 msgid "" "Postr needs to login to Flickr to upload your photos. Please click on the " "link below to login to Flickr." msgstr "" "Postr muss bei Flickr eingeloggt sein um deine Fotos hochladen zu können. Bitte klicke auf den " "Link unten um die bei Flickr einzuloggen." #: src/AuthenticationDialog.py:32 src/AuthenticationDialog.py:34 msgid "Login to Flickr" msgstr "Bei Flickr einloggen" #: src/postr.py:182 #, python-format msgid "You have %s remaining this month" msgstr "Du hast diesen Monat noch %s übrig" #: src/postr.py:194 msgid "Add Photos" msgstr "Fotos hinzufügen" #: src/postr.py:206 msgid "Images" msgstr "Bilder" #: src/postr.py:212 msgid "All Files" msgstr "Alle Dateien" #: src/postr.py:245 msgid "Currently Uploading" msgstr "Lade hoch" #: src/postr.py:246 msgid "Photos are still being uploaded. Are you sure you want to quit?" msgstr "Es werden noch Fotos hochgeladen. Bist du sicher, dass du das Programm verlassen willst?" #: src/postr.py:509 #, python-format msgid "Uploading %(index)d of %(count)d" msgstr "Lade %(index)d von %(count)d hoch" #: nautilus/postrExtension.py:82 msgid "Upload to Flickr..." msgstr "Bei Flickr hochladen..." #: nautilus/postrExtension.py:83 msgid "Upload the selected files into Flickr" msgstr "Lade die ausgewählten Datein zu Flickr" #: src/postr.glade:40 msgid "_File" msgstr "_Datei" #: src/postr.glade:49 msgid "_Add Photos..." msgstr "_Fotos hinzufügen" #: src/postr.glade:77 msgid "_Upload" msgstr "_Hochladen" #: src/postr.glade:117 msgid "Select" msgstr "Auswahl" #: src/postr.glade:127 msgid "_Delete" msgstr "_Löschen" #: src/postr.glade:155 msgid "_Select All" msgstr "_Alle auswählen" #: src/postr.glade:177 msgid "Dese_lect All" msgstr "Alle a_bwählen" #: src/postr.glade:187 msgid "_Invert Selection" msgstr "Auswahl _umkehren" #: src/postr.glade:200 msgid "_Help" msgstr "_Hilfe" #: src/postr.glade:209 msgid "_About" msgstr "_Über" #: src/postr.glade:272 msgid "_Title:" msgstr "_Titel:" #: src/postr.glade:301 msgid "_Description:" msgstr "_Beschreibung:" #: src/postr.glade:330 msgid "Ta_gs:" msgstr "Ta_gs:" #: src/postr.glade:442 msgid "..." msgstr "..." postr-0.12.4/po/it.po0000644000175000017500000000553511274363367013055 0ustar gpoogpoo# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: Postr\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2007-01-10 10:04+0000\n" "PO-Revision-Date: 2007-01-10 10:10+0000\n" "Last-Translator: Emmanuele Bassi \n" "Language-Team: Italian \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: src/AboutDialog.py:7 src/AuthenticationDialog.py:16 src/postr.glade:8 #: src/postr.glade:486 msgid "Flickr Uploader" msgstr "" #: src/AuthenticationDialog.py:19 msgid "Continue" msgstr "Continua" #: src/AuthenticationDialog.py:23 msgid "" "Postr needs to login to Flickr to upload your photos. Please click on the " "link below to login to Flickr." msgstr "" "Postr deve poter registrarsi su Flickr per inviare le tue foto. Premi il " "link sottostante per registrarti su Flickr" #: src/AuthenticationDialog.py:32 src/AuthenticationDialog.py:34 msgid "Login to Flickr" msgstr "Login in Flickr" #: src/postr.py:182 #, python-format msgid "You have %s remaining this month" msgstr "Hai %s rimanenti questo mese" #: src/postr.py:194 msgid "Add Photos" msgstr "Aggiungi foto" #: src/postr.py:206 msgid "Images" msgstr "Immagini" #: src/postr.py:212 msgid "All Files" msgstr "Tutti i file" #: src/postr.py:245 msgid "Currently Uploading" msgstr "Invio corrente" #: src/postr.py:246 msgid "Photos are still being uploaded. Are you sure you want to quit?" msgstr "Le foto sono in fase di caricamento. Sei sicuro di voler uscire?" #: src/postr.py:509 #, python-format msgid "Uploading %(index)d of %(count)d" msgstr "Invio %(index)d di %(count)d" #: nautilus/postrExtension.py:82 msgid "Upload to Flickr..." msgstr "Invio su Flickr..." #: nautilus/postrExtension.py:83 msgid "Upload the selected files into Flickr" msgstr "Invia i file selezionati su Flickr" #: src/postr.glade:40 msgid "_File" msgstr "_File" #: src/postr.glade:49 msgid "_Add Photos..." msgstr "_Aggiungi Foto" #: src/postr.glade:77 msgid "_Upload" msgstr "_Invia" #: src/postr.glade:117 msgid "Select" msgstr "Seleziona" #: src/postr.glade:127 msgid "_Delete" msgstr "_Cancella" #: src/postr.glade:155 msgid "_Select All" msgstr "Seleziona _Tutti" #: src/postr.glade:177 msgid "Dese_lect All" msgstr "De_seleziona Tutti" #: src/postr.glade:187 msgid "_Invert Selection" msgstr "_Inverti Selezione" #: src/postr.glade:200 msgid "_Help" msgstr "_Aiuto" #: src/postr.glade:209 msgid "_About" msgstr "_Informazioni su" #: src/postr.glade:272 msgid "_Title:" msgstr "_Titolo:" #: src/postr.glade:301 msgid "_Description:" msgstr "_Descrizione:" #: src/postr.glade:330 msgid "Ta_gs:" msgstr "Ta_g:" #: src/postr.glade:442 msgid "..." msgstr "..." postr-0.12.4/po/fr.po0000644000175000017500000000563711274363367013053 0ustar gpoogpoo# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: Postr\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2007-01-10 10:04+0000\n" "PO-Revision-Date: 2007-04-06 20:53+0100\n" "Last-Translator: Yoan Blanc \n" "Language-Team: Italian \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: src/AboutDialog.py:7 src/AuthenticationDialog.py:16 src/postr.glade:8 #: src/postr.glade:486 msgid "Flickr Uploader" msgstr "" #: src/AuthenticationDialog.py:19 msgid "Continue" msgstr "Continuer" #: src/AuthenticationDialog.py:23 msgid "" "Postr needs to login to Flickr to upload your photos. Please click on the " "link below to login to Flickr." msgstr "" "Postr nécessite de se logguer à Flickr pour envoyer vos photos. Veuillez " "cliquez sur le lien ci-dessous pour vous logguer à Flickr." #: src/AuthenticationDialog.py:32 src/AuthenticationDialog.py:34 msgid "Login to Flickr" msgstr "Se logguer à Flickr" #: src/postr.py:182 #, python-format msgid "You have %s remaining this month" msgstr "Il vous reste %s ce mois" #: src/postr.py:194 msgid "Add Photos" msgstr "Ajouter des Photos" #: src/postr.py:206 msgid "Images" msgstr "Images" #: src/postr.py:212 msgid "All Files" msgstr "Toutes les fichiers" #: src/postr.py:245 msgid "Currently Uploading" msgstr "Envoi en cours" #: src/postr.py:246 msgid "Photos are still being uploaded. Are you sure you want to quit?" msgstr "Les photos sont en cours d'envoi. Êtes-vous sûr que vous désirez quitter ?" #: src/postr.py:509 #, python-format msgid "Uploading %(index)d of %(count)d" msgstr "Envoi %(index)d de %(count)d" #: nautilus/postrExtension.py:82 msgid "Upload to Flickr..." msgstr "Envoi à Flickr..." #: nautilus/postrExtension.py:83 msgid "Upload the selected files into Flickr" msgstr "Envoyer les ficihers sélectionnés à Flickr" #: src/postr.glade:40 msgid "_File" msgstr "_Fichier" #: src/postr.glade:49 msgid "_Add Photos..." msgstr "_Ajouter des photos..." #: src/postr.glade:77 msgid "_Upload" msgstr "_Envoi" #: src/postr.glade:117 msgid "Select" msgstr "Sélection" #: src/postr.glade:127 msgid "_Delete" msgstr "_Supprimer" #: src/postr.glade:155 msgid "_Select All" msgstr "Tout _sélectionner" #: src/postr.glade:177 msgid "Dese_lect All" msgstr "_Tout supprimer" #: src/postr.glade:187 msgid "_Invert Selection" msgstr "_Inverser la sélection" #: src/postr.glade:200 msgid "_Help" msgstr "_Aide" #: src/postr.glade:209 msgid "_About" msgstr "_A propos" #: src/postr.glade:272 msgid "_Title:" msgstr "_Titre :" #: src/postr.glade:301 msgid "_Description:" msgstr "_Description :" #: src/postr.glade:330 msgid "Ta_gs:" msgstr "Ta_g :" #: src/postr.glade:442 msgid "..." msgstr "..." postr-0.12.4/AUTHORS0000644000175000017500000000024511274363367012524 0ustar gpoogpooAuthor === Ross Burton Contributions === Dean Sas German Poo-Caamano Daniel Stone