Bytes filenames should be accepted with Python 3 #258

Closed
Arfrever opened this Issue Jun 29, 2013 · 6 comments

Comments

Projects
None yet
3 participants

Bytes filenames should be accepted with Python 3. They are accepted with Python 2. An application might want to use bytes filenames to avoid problems with undecodable characters.

$ python2.7 -c 'import PIL.Image; PIL.Image.open(b"file.png")'
$ python3.3 -c 'import PIL.Image; PIL.Image.open(b"file.png")'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/usr/lib64/python3.3/site-packages/PIL/Image.py", line 1993, in open
    prefix = fp.read(16)
AttributeError: 'bytes' object has no attribute 'read'

Also there is NameError in line 491 of PIL/JpegImagePlugin.py with Python 3.

Patch:

--- PIL/_util.py
+++ PIL/_util.py
@@ -0,0 +1,14 @@
+if bytes is str:
+    def isStringType(t):
+        return isinstance(t, basestring)
+    def isPath(f):
+        return isinstance(f, basestring)
+else:
+    def isStringType(t):
+        return isinstance(t, str)
+    def isPath(f):
+        return isinstance(f, (bytes, str))
+
+# Checks if an object is a string, and that it points to a directory.
+def isDirectory(f):
+    return isPath(f) and os.path.isdir(f)
--- PIL/GdImageFile.py
+++ PIL/GdImageFile.py
@@ -26,6 +26,7 @@
 __version__ = "0.1"

 from PIL import ImageFile, ImagePalette, _binary
+from PIL._util import isPath

 try:
     import builtins
@@ -77,7 +78,7 @@ def open(fp, mode = "r"):
     if mode != "r":
         raise ValueError("bad mode")

-    if isinstance(fp, str):
+    if isPath(fp):
         filename = fp
         fp = builtins.open(fp, "rb")
     else:
--- PIL/Image.py
+++ PIL/Image.py
@@ -58,7 +58,7 @@ try:
          raise ImportError("The _imaging extension was built for another "
                             " version of Pillow or PIL. Most PIL functions "
                              " will be disabled ")
-             
+
 except ImportError as v:
     core = _imaging_not_installed()
     if str(v)[:20] == "Module use of python" and warnings:
@@ -81,6 +81,7 @@ except ImportError:

 from PIL import ImageMode
 from PIL._binary import i8, o8
+from PIL._util import isPath, isStringType

 import os, sys

@@ -88,26 +89,12 @@ import os, sys
 import collections
 import numbers

-if bytes is str:
-    def isStringType(t):
-        return isinstance(t, basestring)
-else:
-    def isStringType(t):
-        return isinstance(t, str)
-
 ##
 # (Internal) Checks if an object is an image object.

 def isImageType(t):
     return hasattr(t, "im")

-##
-# (Internal) Checks if an object is a string, and that it points to a
-# directory.
-
-def isDirectory(f):
-    return isStringType(f) and os.path.isdir(f)
-
 #
 # Debug level

@@ -1421,10 +1408,10 @@ class Image:
     def save(self, fp, format=None, **params):
         "Save image to file or stream"

-        if isStringType(fp):
+        if isPath(fp):
             filename = fp
         else:
-            if hasattr(fp, "name") and isStringType(fp.name):
+            if hasattr(fp, "name") and isPath(fp.name):
                 filename = fp.name
             else:
                 filename = ""
@@ -1455,7 +1442,7 @@ class Image:
             init()
             save_handler = SAVE[format.upper()] # unknown format

-        if isStringType(fp):
+        if isPath(fp):
             fp = builtins.open(fp, "wb")
             close = 1
         else:
@@ -1984,7 +1971,7 @@ def open(fp, mode="r"):
     if mode != "r":
         raise ValueError("bad mode")

-    if isStringType(fp):
+    if isPath(fp):
         filename = fp
         fp = builtins.open(fp, "rb")
     else:
--- PIL/ImageCms.py
+++ PIL/ImageCms.py
@@ -83,6 +83,7 @@ VERSION = "0.1.0 pil"

 from PIL import Image
 from PIL import _imagingcms
+from PIL._util import isStringType

 core = _imagingcms

@@ -139,7 +140,7 @@ class ImageCmsProfile:
     def __init__(self, profile):
         # accepts a string (filename), a file-like object, or a low-level
         # profile object
-        if Image.isStringType(profile):
+        if isStringType(profile):
             self._set(core.profile_open(profile), profile)
         elif hasattr(profile, "read"):
             self._set(core.profile_frombytes(profile.read()))
--- PIL/ImageDraw.py
+++ PIL/ImageDraw.py
@@ -33,6 +33,7 @@
 import numbers

 from PIL import Image, ImageColor
+from PIL._util import isStringType

 try:
     import warnings
@@ -98,7 +99,7 @@ class ImageDraw:
                 "'setink' is deprecated; use keyword arguments instead",
                 DeprecationWarning, stacklevel=2
                 )
-        if Image.isStringType(ink):
+        if isStringType(ink):
             ink = ImageColor.getcolor(ink, self.mode)
         if self.palette and not isinstance(ink, numbers.Number):
             ink = self.palette.getcolor(ink)
@@ -141,13 +142,13 @@ class ImageDraw:
                 ink = self.ink
         else:
             if ink is not None:
-                if Image.isStringType(ink):
+                if isStringType(ink):
                     ink = ImageColor.getcolor(ink, self.mode)
                 if self.palette and not isinstance(ink, numbers.Number):
                     ink = self.palette.getcolor(ink)
                 ink = self.draw.draw_ink(ink, self.mode)
             if fill is not None:
-                if Image.isStringType(fill):
+                if isStringType(fill):
                     fill = ImageColor.getcolor(fill, self.mode)
                 if self.palette and not isinstance(fill, numbers.Number):
                     fill = self.palette.getcolor(fill)
--- PIL/ImageFile.py
+++ PIL/ImageFile.py
@@ -28,6 +28,7 @@
 #

 from PIL import Image
+from PIL._util import isPath
 import traceback, os
 import io

@@ -81,7 +82,7 @@ class ImageFile(Image.Image):
         self.decoderconfig = ()
         self.decodermaxblock = MAXBLOCK

-        if Image.isStringType(fp):
+        if isPath(fp):
             # filename
             self.fp = open(fp, "rb")
             self.filename = fp
--- PIL/ImageFont.py
+++ PIL/ImageFont.py
@@ -28,6 +28,7 @@
 from __future__ import print_function

 from PIL import Image
+from PIL._util import isDirectory, isPath
 import os, sys

 try:
@@ -45,13 +46,6 @@ try:
 except ImportError:
     core = _imagingft_not_installed()

-if bytes is str:
-    def isStringType(t):
-        return isinstance(t, basestring)
-else:
-    def isStringType(t):
-        return isinstance(t, str)
-
 # FIXME: add support for pilfont2 format (see FontFile.py)

 # --------------------------------------------------------------------
@@ -148,7 +142,7 @@ class FreeTypeFont:
                 warnings.warn('file parameter deprecated, please use font parameter instead.', DeprecationWarning)
             font = file

-        if isStringType(font):
+        if isPath(font):
             self.font = core.getfont(font, size, index, encoding)
         else:
             self.font_bytes = font.read()
@@ -266,7 +260,12 @@ def truetype(font=None, size=10, index=0, encoding="", filename=None):
 def load_path(filename):
     "Load a font file, searching along the Python path."
     for dir in sys.path:
-        if Image.isDirectory(dir):
+        if isDirectory(dir):
+            if not isinstance(filename, "utf-8"):
+                if bytes is str:
+                    filename = filename.encode("utf-8")
+                else:
+                    filename = filename.decode("utf-8")
             try:
                 return load(os.path.join(dir, filename))
             except IOError:
--- PIL/ImageOps.py
+++ PIL/ImageOps.py
@@ -18,6 +18,7 @@
 #

 from PIL import Image
+from PIL._util import isStringType
 import operator
 from functools import reduce

@@ -43,7 +44,7 @@ def _border(border):
     return left, top, right, bottom

 def _color(color, mode):
-    if Image.isStringType(color):
+    if isStringType(color):
         from PIL import ImageColor
         color = ImageColor.getcolor(color, mode)
     return color
--- PIL/ImageQt.py
+++ PIL/ImageQt.py
@@ -16,6 +16,7 @@
 #

 from PIL import Image
+from PIL._util import isPath

 from PyQt4.QtGui import QImage, qRgb

@@ -45,7 +46,7 @@ class ImageQt(QImage):
         if hasattr(im, "toUtf8"):
             # FIXME - is this really the best way to do this?
             im = unicode(im.toUtf8(), "utf-8")
-        if Image.isStringType(im):
+        if isPath(im):
             im = Image.open(im)

         if im.mode == "1":
--- PIL/JpegImagePlugin.py
+++ PIL/JpegImagePlugin.py
@@ -37,6 +37,7 @@ __version__ = "0.6"
 import array, struct
 from PIL import Image, ImageFile, _binary
 from PIL.JpegPresets import presets
+from PIL._util import isStringType

 i8 = _binary.i8
 o8 = _binary.o8
@@ -488,7 +489,7 @@ def _save(im, fp, filename):
     def validate_qtables(qtables):
         if qtables is None:
             return qtables
-        if isinstance(qtables, basestring):
+        if isStringType(qtables):
             try:
                 lines = [int(num) for line in qtables.splitlines()
                          for num in line.split('#', 1)[0].split()]
--- PIL/OleFileIO.py
+++ PIL/OleFileIO.py
@@ -41,6 +41,7 @@ from __future__ import print_function
 import io
 import sys
 from PIL import _binary
+from PIL._util import isPath

 if str is not bytes:
     long = int
@@ -269,7 +270,7 @@ class OleFileIO:
     def open(self, filename):
         """Open an OLE2 file"""

-        if isinstance(filename, str):
+        if isPath(filename):
             self.fp = open(filename, "rb")
         else:
             self.fp = filename
Owner

aclark4life commented Jun 29, 2013

@wiredfool Any interest in applying this patch?

http://docs.python.org/3.3/library/os.path.html says:
"Unfortunately, some file names may not be representable as strings on Unix, so applications that need to support arbitrary file names on Unix should use bytes objects to represent path names. Vice versa, using bytes objects cannot represent all file names on Windows (in the standard mbcs encoding), hence Windows applications should use string objects to access all files."

Owner

aclark4life commented Jun 30, 2013

One hunk failed, perhaps erroneously, but please double check:

aclark@Alexs-MacBook-Pro:~/Developer/Pillow/ > patch -p0 < patch.diff 
patching file PIL/_util.py
patching file PIL/GdImageFile.py
patching file PIL/Image.py
Hunk #1 FAILED at 58.
1 out of 6 hunks FAILED -- saving rejects to file PIL/Image.py.rej
patching file PIL/ImageCms.py
patching file PIL/ImageDraw.py
patching file PIL/ImageFile.py
patching file PIL/ImageFont.py
patching file PIL/ImageOps.py
patching file PIL/ImageQt.py
patching file PIL/JpegImagePlugin.py
patching file PIL/OleFileIO.py

Here is PIL/Image.py.rej


aclark@Alexs-MacBook-Pro:~/Developer/Pillow/ > cat PIL/Image.py.rej 
***************
*** 58,64 ****
           raise ImportError("The _imaging extension was built for another "
                              " version of Pillow or PIL. Most PIL functions "
                               " will be disabled ")
-              
  except ImportError as v:
      core = _imaging_not_installed()
      if str(v)[:20] == "Module use of python" and warnings:
--- 58,64 ----
           raise ImportError("The _imaging extension was built for another "
                              " version of Pillow or PIL. Most PIL functions "
                               " will be disabled ")
+ 
  except ImportError as v:
      core = _imaging_not_installed()
      if str(v)[:20] == "Module use of python" and warnings:
Owner

wiredfool commented Jun 30, 2013

fd29e70 broke the build.

Perhaps you forgot to add _util?

wiredfool reopened this Jun 30, 2013

Owner

aclark4life commented Jun 30, 2013

Yep, thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment