[Zope] Zope 2.4.3 & PDFs = BUG?

Dirk Datzert Dirk.Datzert@rasselstein-hoesch.de
Tue, 8 Jan 2002 10:10:13 +0100


This is a multi-part message in MIME format.

------=_NextPart_000_0047_01C1982C.A9E2F190
Content-Type: text/plain;
	charset="iso-8859-1"
Content-Transfer-Encoding: 7bit

This bug-fix works for mine 2.4.3

----- Original Message -----
From: "Martijn Pieters" <mj@zope.com>
To: "Joachim Werner" <joe@iuveno-net.de>
Cc: <zope-dev@zope.org>
Sent: Monday, January 07, 2002 10:12 PM
Subject: Re: [Zope-dev] PDF-specific Bug in the ZServer implementation??? Or
just strange behavoiur of IE?


> On Mon, Jan 07, 2002 at 09:56:40PM +0100, Joachim Werner wrote:
> > What is the best approach to upgrading to the new code? Replacing the
> > ZServer code by the CVS one?
>
> The code only applies to OFS/Image.py (only File and Image objects support
> HTTP Range) and ZPublisher/HTTPRangeSupport.py. I've attached the versions
> for a Zope 2.4.x installation, just drop them in the correct places in
your
> current Zope setup.
>
> --
> Martijn Pieters
> | Software Engineer  mailto:mj@zope.com
> | Zope Corporation   http://www.zope.com/
> | Creators of Zope   http://www.zope.org/
> ---------------------------------------------
>

------=_NextPart_000_0047_01C1982C.A9E2F190
Content-Type: application/octet-stream;
	name="Image.py"
Content-Transfer-Encoding: quoted-printable
Content-Disposition: attachment;
	filename="Image.py"

#########################################################################=
#####
#=20
# Zope Public License (ZPL) Version 1.0
# -------------------------------------
#=20
# Copyright (c) Digital Creations.  All rights reserved.
#=20
# This license has been certified as Open Source(tm).
#=20
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#=20
# 1. Redistributions in source code must retain the above copyright
#    notice, this list of conditions, and the following disclaimer.
#=20
# 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.
#=20
# 3. Digital Creations requests that attribution be given to Zope
#    in any manner possible. Zope includes a "Powered by Zope"
#    button that is installed by default. While it is not a license
#    violation to remove this button, it is requested that the
#    attribution remain. A significant investment has been put
#    into Zope, and this effort will continue if the Zope community
#    continues to grow. This is one way to assure that growth.
#=20
# 4. All advertising materials and documentation mentioning
#    features derived from or use of this software must display
#    the following acknowledgement:
#=20
#      "This product includes software developed by Digital Creations
#      for use in the Z Object Publishing Environment
#      (http://www.zope.org/)."
#=20
#    In the event that the product being advertised includes an
#    intact Zope distribution (with copyright and license included)
#    then this clause is waived.
#=20
# 5. Names associated with Zope or Digital Creations must not be used to
#    endorse or promote products derived from this software without
#    prior written permission from Digital Creations.
#=20
# 6. Modified redistributions of any form whatsoever must retain
#    the following acknowledgment:
#=20
#      "This product includes software developed by Digital Creations
#      for use in the Z Object Publishing Environment
#      (http://www.zope.org/)."
#=20
#    Intact (re-)distributions of any official Zope release do not
#    require an external acknowledgement.
#=20
# 7. Modifications are encouraged but must be packaged separately as
#    patches to official Zope releases.  Distributions that do not
#    clearly separate the patches from the original work must be clearly
#    labeled as unofficial distributions.  Modifications which do not
#    carry the name Zope may be packaged in any form, as long as they
#    conform to all of the clauses above.
#=20
#=20
# Disclaimer
#=20
#   THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
#   EXPRESSED 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 DIGITAL CREATIONS OR ITS
#   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.
#=20
#=20
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations.  Specific
# attributions are listed in the accompanying credits file.
#=20
#########################################################################=
#####
"""Image object"""

__version__=3D'$Revision: 1.130.4.2 $'[11:-2]

import Globals, string, struct
from OFS.content_types import guess_content_type
from Globals import DTMLFile
from PropertyManager import PropertyManager
from AccessControl.Role import RoleManager
from webdav.common import rfc1123_date
from webdav.Lockable import ResourceLockedError
from webdav.WriteLockInterface import WriteLockInterface
from SimpleItem import Item_w__name__
from cStringIO import StringIO
from Globals import Persistent
from Acquisition import Implicit
from DateTime import DateTime
from Cache import Cacheable
from mimetools import choose_boundary
from ZPublisher import HTTPRangeSupport

StringType=3Dtype('')
manage_addFileForm=3DDTMLFile('dtml/imageAdd', =
globals(),Kind=3D'File',kind=3D'file')
def manage_addFile(self,id,file=3D'',title=3D'',precondition=3D'', =
content_type=3D'',
                   REQUEST=3DNone):
    """Add a new File object.

    Creates a new File object 'id' with the contents of 'file'"""

    id=3Dstr(id)
    title=3Dstr(title)
    content_type=3Dstr(content_type)
    precondition=3Dstr(precondition)
   =20
    id, title =3D cookId(id, title, file)

    self=3Dself.this()

    # First, we create the file without data:
    self._setObject(id, File(id,title,'',content_type, precondition))

    # Now we "upload" the data.  By doing this in two steps, we
    # can use a database trick to make the upload more efficient.
    self._getOb(id).manage_upload(file)
    if content_type:
        self._getOb(id).content_type=3Dcontent_type

    if REQUEST is not None:
        REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_main')


class File(Persistent, Implicit, PropertyManager,
           RoleManager, Item_w__name__, Cacheable):
    """A File object is a content object for arbitrary files."""
   =20
    __implements__ =3D (WriteLockInterface, =
HTTPRangeSupport.HTTPRangeInterface)
    meta_type=3D'File'

   =20
    precondition=3D''
    size=3DNone

    manage_editForm  =3DDTMLFile('dtml/fileEdit',globals(),
                               Kind=3D'File',kind=3D'file')
    manage_editForm._setName('manage_editForm')
    manage=3Dmanage_main=3Dmanage_editForm
    manage_uploadForm=3Dmanage_editForm
   =20
    manage_options=3D(
        (
        {'label':'Edit', 'action':'manage_main',
         'help':('OFSP','File_Edit.stx')},
        {'label':'View', 'action':'',
         'help':('OFSP','File_View.stx')},
        )
        + PropertyManager.manage_options
        + RoleManager.manage_options
        + Item_w__name__.manage_options
        + Cacheable.manage_options
        )


    __ac_permissions__=3D(
        ('View management screens',
         ('manage', 'manage_main',)),
        ('Change Images and Files',
         ('manage_edit','manage_upload','PUT')),
        ('View',
         ('index_html', 'view_image_or_file', 'get_size',
          'getContentType', '')),
        ('FTP access',
         ('manage_FTPstat','manage_FTPget','manage_FTPlist')),
        ('Delete objects',
         ('DELETE',)),
        )
  =20

    _properties=3D({'id':'title', 'type': 'string'},
                 {'id':'content_type', 'type':'string'},
                 )

    def __init__(self, id, title, file, content_type=3D'', =
precondition=3D''):
        self.__name__=3Did
        self.title=3Dtitle
        self.precondition=3Dprecondition
      =20
        data, size =3D self._read_data(file)
        content_type=3Dself._get_content_type(file, data, id, =
content_type)
        self.update_data(data, content_type, size)

    def id(self):
        return self.__name__

    def index_html(self, REQUEST, RESPONSE):
        """
        The default view of the contents of a File or Image.

        Returns the contents of the file or image.  Also, sets the
        Content-Type HTTP header to the objects content type.
        """
        # HTTP If-Modified-Since header handling.
        header=3DREQUEST.get_header('If-Modified-Since', None)
        if header is not None:
            header=3Dstring.split(header, ';')[0]
            # Some proxies seem to send invalid date strings for this
            # header. If the date string is not valid, we ignore it
            # rather than raise an error to be generally consistent
            # with common servers such as Apache (which can usually
            # understand the screwy date string as a lucky side effect
            # of the way they parse it).
            # This happens to be what RFC2616 tells us to do in the face =
of an
            # invalid date.
            try:    mod_since=3Dlong(DateTime(header).timeTime())
            except: mod_since=3DNone
            if mod_since is not None:
                if self._p_mtime:
                    last_mod =3D long(self._p_mtime)
                else:
                    last_mod =3D long(0)
                if last_mod > 0 and last_mod <=3D mod_since:
                    # Set header values since apache caching will return =
Content-Length
                    # of 0 in response if size is not set here
                    RESPONSE.setHeader('Last-Modified', =
rfc1123_date(self._p_mtime))
                    RESPONSE.setHeader('Content-Type', =
self.content_type)
                    RESPONSE.setHeader('Content-Length', self.size)
                    RESPONSE.setHeader('Accept-Ranges', 'bytes')
                    RESPONSE.setStatus(304)
                    return ''

        if self.precondition and hasattr(self,self.precondition):
            # Grab whatever precondition was defined and then=20
            # execute it.  The precondition will raise an exception=20
            # if something violates its terms.
            c=3Dgetattr(self,self.precondition)
            if hasattr(c,'isDocTemp') and c.isDocTemp:
                c(REQUEST['PARENTS'][1],REQUEST)
            else:
                c()

        # HTTP Range header handling
        range =3D REQUEST.get_header('Range', None)
        request_range =3D REQUEST.get_header('Request-Range', None)
        if request_range is not None:
            # Netscape 2 through 4 and MSIE 3 implement a draft version
            # Later on, we need to serve a different mime-type as well.
            range =3D request_range
        if_range =3D REQUEST.get_header('If-Range', None)
        if range is not None:
            ranges =3D HTTPRangeSupport.parseRange(range)

            if if_range is not None:
                # Only send ranges if the data isn't modified, otherwise =
send
                # the whole object. Support both ETags and Last-Modified =
dates!
                if len(if_range) > 1 and if_range[:2] =3D=3D 'ts':
                    # ETag:
                    if if_range !=3D self.http__etag():
                        # Modified, so send a normal response. We delete
                        # the ranges, which causes us to skip to the 200
                        # response.
                        ranges =3D None
                else:
                    # Date
                    date =3D string.split(if_range, ';')[0]
                    try: mod_since=3Dlong(DateTime(date).timeTime())
                    except: mod_since=3DNone
                    if mod_since is not None:
                        if self._p_mtime:
                            last_mod =3D long(self._p_mtime)
                        else:
                            last_mod =3D long(0)
                        if last_mod > mod_since:
                            # Modified, so send a normal response. We =
delete
                            # the ranges, which causes us to skip to the =
200
                            # response.
                            ranges =3D None

            if ranges:
                # Search for satisfiable ranges.
                satisfiable =3D 0
                for start, end in ranges:
                    if start < self.size:
                        satisfiable =3D 1
                        break

                if not satisfiable:
                    RESPONSE.setHeader('Content-Range',=20
                        'bytes */%d' % self.size)
                    RESPONSE.setHeader('Accept-Ranges', 'bytes')
                    RESPONSE.setHeader('Last-Modified',
                        rfc1123_date(self._p_mtime))
                    RESPONSE.setHeader('Content-Type', =
self.content_type)
                    RESPONSE.setHeader('Content-Length', self.size)
                    RESPONSE.setStatus(416)
                    return ''

                # Can we optimize?
                ranges =3D HTTPRangeSupport.optimizeRanges(ranges, =
self.size)
                               =20
                if len(ranges) =3D=3D 1:
                    # Easy case, set extra header and return partial =
set.
                    start, end =3D ranges[0]
                    size =3D end - start
                   =20
                    RESPONSE.setHeader('Last-Modified',
                        rfc1123_date(self._p_mtime))
                    RESPONSE.setHeader('Content-Type', =
self.content_type)
                    RESPONSE.setHeader('Content-Length', size)
                    RESPONSE.setHeader('Accept-Ranges', 'bytes')
                    RESPONSE.setHeader('Content-Range',=20
                        'bytes %d-%d/%d' % (start, end - 1, self.size))
                    RESPONSE.setStatus(206) # Partial content

                    data =3D self.data
                    if type(data) is StringType:
                        return data[start:end]

                    # Linked Pdata objects. Urgh.
                    pos =3D 0
                    while data is not None:
                        l =3D len(data.data)
                        pos =3D pos + l
                        if pos > start:
                            # We are within the range
                            lstart =3D l - (pos - start)

                            if lstart < 0: lstart =3D 0
                           =20
                            # find the endpoint
                            if end <=3D pos:
                                lend =3D l - (pos - end)
                               =20
                                # Send and end transmission
                                RESPONSE.write(data[lstart:lend])
                                break

                            # Not yet at the end, transmit what we have.
                            RESPONSE.write(data[lstart:])

                        data =3D data.next
                   =20
                    return ''
                   =20
                else:
                    # When we get here, ranges have been optimized, so =
they are
                    # in order, non-overlapping, and start and end =
values are
                    # positive integers.
                    boundary =3D choose_boundary()
                   =20
                    # Calculate the content length
                    size =3D (8 + len(boundary) + # End marker length
                        len(ranges) * (         # Constant lenght per =
set
                            49 + len(boundary) + len(self.content_type) =
+=20
                            len('%d' % self.size)))
                    for start, end in ranges:
                        # Variable length per set
                        size =3D (size + len('%d%d' % (start, end - 1)) =
+=20
                            end - start)
                           =20
                   =20
                    # Some clients implement an earlier draft of the =
spec, they
                    # will only accept x-byteranges.
                    draftprefix =3D (request_range is not None) and 'x-' =
or ''

                    RESPONSE.setHeader('Content-Length', size)
                    RESPONSE.setHeader('Accept-Ranges', 'bytes')
                    RESPONSE.setHeader('Last-Modified',
                        rfc1123_date(self._p_mtime))
                    RESPONSE.setHeader('Content-Type',
                        'multipart/%sbyteranges; boundary=3D%s' % (
                            draftprefix, boundary))
                    RESPONSE.setStatus(206) # Partial content

                    pos =3D 0
                    data =3D self.data

                    for start, end in ranges:
                        RESPONSE.write('\r\n--%s\r\n' % boundary)
                        RESPONSE.write('Content-Type: %s\r\n' %
                            self.content_type)
                        RESPONSE.write(
                            'Content-Range: bytes %d-%d/%d\r\n\r\n' % (
                                start, end - 1, self.size))=20

                        if type(data) is StringType:
                            RESPONSE.write(data[start:end])

                        else:
                            # Yippee. Linked Pdata objects.
                            while data is not None:
                                l =3D len(data.data)
                                pos =3D pos + l
                                if pos > start:
                                    # We are within the range
                                    lstart =3D l - (pos - start)

                                    if lstart < 0: lstart =3D 0
                                   =20
                                    # find the endpoint
                                    if end <=3D pos:
                                        lend =3D l - (pos - end)
                                       =20
                                        # Send and loop to next range
                                        =
RESPONSE.write(data[lstart:lend])
                                        # Back up the position marker, =
it will
                                        # be incremented again for the =
next
                                        # part.
                                        pos =3D pos - l
                                        break

                                    # Not yet at the end, transmit what =
we have.
                                    RESPONSE.write(data[lstart:])

                                data =3D data.next

                    RESPONSE.write('\r\n--%s--\r\n' % boundary)
                    return ''

        RESPONSE.setHeader('Last-Modified', rfc1123_date(self._p_mtime))
        RESPONSE.setHeader('Content-Type', self.content_type)
        RESPONSE.setHeader('Content-Length', self.size)
        RESPONSE.setHeader('Accept-Ranges', 'bytes')

        # Don't cache the data itself, but provide an opportunity
        # for a cache manager to set response headers.
        self.ZCacheable_set(None)

        data=3Dself.data
        if type(data) is type(''): return data

        while data is not None:
            RESPONSE.write(data.data)
            data=3Ddata.next

        return ''

    def view_image_or_file(self, URL1):
        """
        The default view of the contents of the File or Image.
        """
        raise 'Redirect', URL1

    # private
    update_data__roles__=3D()
    def update_data(self, data, content_type=3DNone, size=3DNone):
        if content_type is not None: self.content_type=3Dcontent_type
        if size is None: size=3Dlen(data)
        self.size=3Dsize
        self.data=3Ddata
        self.ZCacheable_invalidate()
        self.http__refreshEtag()

    def manage_edit(self, title, content_type, precondition=3D'', =
REQUEST=3DNone):
        """
        Changes the title and content type attributes of the File or =
Image.
        """
        if self.wl_isLocked():
            raise ResourceLockedError, "File is locked via WebDAV"

        self.title=3Dstr(title)
        self.content_type=3Dstr(content_type)
        if precondition: self.precondition=3Dstr(precondition)
        elif self.precondition: del self.precondition
        self.ZCacheable_invalidate()
        if REQUEST:
            message=3D"Saved changes."
            return =
self.manage_main(self,REQUEST,manage_tabs_message=3Dmessage)

    def manage_upload(self,file=3D'',REQUEST=3DNone):
        """
        Replaces the current contents of the File or Image object with =
file.

        The file or images contents are replaced with the contents of =
'file'.
        """
        if self.wl_isLocked():
            raise ResourceLockedError, "File is locked via WebDAV"

        data, size =3D self._read_data(file)
        content_type=3Dself._get_content_type(file, data, self.__name__,
                                            'application/octet-stream')
        self.update_data(data, content_type, size)

        if REQUEST:
            message=3D"Saved changes."
            return =
self.manage_main(self,REQUEST,manage_tabs_message=3Dmessage)
       =20
    def _get_content_type(self, file, body, id, content_type=3DNone):
        headers=3Dgetattr(file, 'headers', None)
        if headers and headers.has_key('content-type'):
            content_type=3Dheaders['content-type']
        else:
            if type(body) is not type(''): body=3Dbody.data
            content_type, enc=3Dguess_content_type(
                getattr(file, 'filename',id), body, content_type)
        return content_type

    def _read_data(self, file):
       =20
        n=3D1 << 16
       =20
        if type(file) is StringType:
            size=3Dlen(file)
            if size < n: return file, size
            return Pdata(file), size

        if hasattr(file, '__class__') and file.__class__ is Pdata:
            size=3Dlen(file)
            return file, size

        seek=3Dfile.seek
        read=3Dfile.read
       =20
        seek(0,2)
        size=3Dend=3Dfile.tell()

        if size <=3D 2*n:
            seek(0)
            if size < n: return read(size), size
            return Pdata(read(size)), size

        # Make sure we have an _p_jar, even if we are a new object, by
        # doing a sub-transaction commit.
        get_transaction().commit(1)
       =20
        jar=3Dself._p_jar
       =20
        if jar is None:
            # Ugh
            seek(0)
            return Pdata(read(size)), size

        # Now we're going to build a linked list from back
        # to front to minimize the number of database updates
        # and to allow us to get things out of memory as soon as
        # possible.
        next=3DNone
        while end > 0:
            pos=3Dend-n
            if pos < n: pos=3D0 # we always want at least n bytes
            seek(pos)
            data=3DPdata(read(end-pos))
           =20
            # Woooop Woooop Woooop! This is a trick.
            # We stuff the data directly into our jar to reduce the
            # number of updates necessary.
            data._p_jar=3Djar

            # This is needed and has side benefit of getting
            # the thing registered:
            data.next=3Dnext
           =20
            # Now make it get saved in a sub-transaction!
            get_transaction().commit(1)

            # Now make it a ghost to free the memory.  We
            # don't need it anymore!
            data._p_changed=3DNone
           =20
            next=3Ddata
            end=3Dpos
       =20
        return next, size

    def PUT(self, REQUEST, RESPONSE):
        """Handle HTTP PUT requests"""
        self.dav__init(REQUEST, RESPONSE)
        self.dav__simpleifhandler(REQUEST, RESPONSE, refresh=3D1)
        type=3DREQUEST.get_header('content-type', None)

        file=3DREQUEST['BODYFILE']
       =20
        data, size =3D self._read_data(file)
        content_type=3Dself._get_content_type(file, data, self.__name__,
                                            type or self.content_type)
        self.update_data(data, content_type, size)

        RESPONSE.setStatus(204)
        return RESPONSE
   =20
    def get_size(self):
        """Get the size of a file or image.

        Returns the size of the file or image.
        """
        size=3Dself.size
        if size is None: size=3Dlen(self.data)
        return size

    # deprecated; use get_size!
    getSize=3Dget_size

    def getContentType(self):
        """Get the content type of a file or image.

        Returns the content type (MIME type) of a file or image.
        """
        return self.content_type


    def __str__(self): return str(self.data)
    def __len__(self): return 1
   =20
    manage_FTPget=3Dindex_html


manage_addImageForm=3DDTMLFile('dtml/imageAdd',globals(),
                             Kind=3D'Image',kind=3D'image')
def manage_addImage(self, id, file, title=3D'', precondition=3D'', =
content_type=3D'',
                    REQUEST=3DNone):
    """
    Add a new Image object.

    Creates a new Image object 'id' with the contents of 'file'.
    """

    id=3Dstr(id)
    title=3Dstr(title)
    content_type=3Dstr(content_type)
    precondition=3Dstr(precondition)

    id, title =3D cookId(id, title, file)

    self=3Dself.this()

    # First, we create the image without data:
    self._setObject(id, Image(id,title,'',content_type, precondition))
       =20
    # Now we "upload" the data.  By doing this in two steps, we
    # can use a database trick to make the upload more efficient.
    self._getOb(id).manage_upload(file)
    if content_type:
        self._getOb(id).content_type=3Dcontent_type
   =20
    if REQUEST is not None:
        try:    url=3Dself.DestinationURL()
        except: url=3DREQUEST['URL1']
        REQUEST.RESPONSE.redirect('%s/manage_main' % url)
    return id


def getImageInfo(data):
    data =3D str(data)
    size =3D len(data)
    height =3D -1
    width =3D -1
    content_type =3D ''

    # handle GIFs  =20
    if (size >=3D 10) and data[:6] in ('GIF87a', 'GIF89a'):
        # Check to see if content_type is correct
        content_type =3D 'image/gif'
        w, h =3D struct.unpack("<HH", data[6:10])
        width =3D int(w)
        height =3D int(h)

    # See PNG v1.2 spec (http://www.cdrom.com/pub/png/spec/)
    # Bytes 0-7 are below, 4-byte chunk length, then 'IHDR'
    # and finally the 4-byte width, height
    elif ((size >=3D 24) and (data[:8] =3D=3D '\211PNG\r\n\032\n')
          and (data[12:16] =3D=3D 'IHDR')):
        content_type =3D 'image/png'
        w, h =3D struct.unpack(">LL", data[16:24])
        width =3D int(w)
        height =3D int(h)
           =20
    # Maybe this is for an older PNG version.
    elif (size >=3D 16) and (data[:8] =3D=3D '\211PNG\r\n\032\n'):
        # Check to see if we have the right content type
        content_type =3D 'image/png'
        w, h =3D struct.unpack(">LL", data[8:16])
        width =3D int(w)
        height =3D int(h)

    # handle JPEGs
    elif (size >=3D 2) and (data[:2] =3D=3D '\377\330'):
        content_type =3D 'image/jpeg'
        jpeg =3D StringIO(data)
        jpeg.read(2)
        b =3D jpeg.read(1)
        try:
            while (b and ord(b) !=3D 0xDA):
                while (ord(b) !=3D 0xFF): b =3D jpeg.read(1)
                while (ord(b) =3D=3D 0xFF): b =3D jpeg.read(1)
                if (ord(b) >=3D 0xC0 and ord(b) <=3D 0xC3):
                    jpeg.read(3)
                    h, w =3D struct.unpack(">HH", jpeg.read(4))
                    break
                else:
                    jpeg.read(int(struct.unpack(">H", =
jpeg.read(2))[0])-2)
                b =3D jpeg.read(1)
            width =3D int(w)
            height =3D int(h)
        except: pass

    return content_type, width, height


class Image(File):
    """Image objects can be GIF, PNG or JPEG and have the same methods
    as File objects.  Images also have a string representation that
    renders an HTML 'IMG' tag.
    """
    __implements__ =3D (WriteLockInterface,)
    meta_type=3D'Image'

   =20
    height=3D''
    width=3D''

    __ac_permissions__=3D(
        ('View management screens',
         ('manage', 'manage_main',)),
        ('Change Images and Files',
         ('manage_edit','manage_upload','PUT')),
        ('View',
         ('index_html', 'tag', 'view_image_or_file', 'get_size',
          'getContentType', '')),
        ('FTP access',
         ('manage_FTPstat','manage_FTPget','manage_FTPlist')),
        ('Delete objects',
         ('DELETE',)),
        )

    _properties=3D({'id':'title', 'type': 'string'},
                 {'id':'content_type', 'type':'string'},
                 {'id':'height', 'type':'string'},
                 {'id':'width', 'type':'string'},
                 )

    manage_options=3D(
        ({'label':'Edit', 'action':'manage_main',
         'help':('OFSP','Image_Edit.stx')},
         {'label':'View', 'action':'view_image_or_file',
         'help':('OFSP','Image_View.stx')},)
        + PropertyManager.manage_options
        + RoleManager.manage_options
        + Item_w__name__.manage_options
        + Cacheable.manage_options
        )

    manage_editForm  =3DDTMLFile('dtml/imageEdit',globals(),
                               Kind=3D'Image',kind=3D'image')
    view_image_or_file =3DDTMLFile('dtml/imageView',globals())
    manage_editForm._setName('manage_editForm')
    manage=3Dmanage_main=3Dmanage_editForm
    manage_uploadForm=3Dmanage_editForm
   =20
    # private
    update_data__roles__=3D()
    def update_data(self, data, content_type=3DNone, size=3DNone):
        if size is None: size=3Dlen(data)

        self.size=3Dsize
        self.data=3Ddata

        ct, width, height =3D getImageInfo(data)
        if ct:
            content_type =3D ct
        if width >=3D 0 and height >=3D 0:
            self.width =3D width
            self.height =3D height

        # Now we should have the correct content type, or still None
        if content_type is not None: self.content_type =3D content_type

        self.ZCacheable_invalidate()


    def __str__(self):
        return self.tag()

    def tag(self, height=3DNone, width=3DNone, alt=3DNone,
            scale=3D0, xscale=3D0, yscale=3D0, css_class=3DNone, =
**args):
        """
        Generate an HTML IMG tag for this image, with customization.
        Arguments to self.tag() can be any valid attributes of an IMG =
tag.
        'src' will always be an absolute pathname, to prevent redundant
        downloading of images. Defaults are applied intelligently for
        'height', 'width', and 'alt'. If specified, the 'scale', =
'xscale',
        and 'yscale' keyword arguments will be used to automatically =
adjust
        the output height and width values of the image tag.

        Since 'class' is a Python reserved word, it cannot be passed in
        directly in keyword arguments which is a problem if you are
        trying to use 'tag()' to include a CSS class. The tag() method
        will accept a 'css_class' argument that will be converted to
        'class' in the output tag to work around this.
        """
        if height is None: height=3Dself.height
        if width is None:  width=3Dself.width

        # Auto-scaling support
        xdelta =3D xscale or scale
        ydelta =3D yscale or scale

        if xdelta and width:
            width =3D  str(int(round(int(width) * xdelta)))
        if ydelta and height:
            height =3D str(int(round(int(height) * ydelta)))

        result=3D'<img src=3D"%s"' % (self.absolute_url())

        if alt is None:
            alt=3Dgetattr(self, 'title', '')
        result =3D '%s alt=3D"%s"' % (result, alt)

        if height:
            result =3D '%s height=3D"%s"' % (result, height)

        if width:
            result =3D '%s width=3D"%s"' % (result, width)

        if not 'border' in map(string.lower, args.keys()):
            result =3D '%s border=3D"0"' % result

        if css_class is not None:
            result =3D '%s class=3D"%s"' % (result, css_class)

        for key in args.keys():
            value =3D args.get(key)
            result =3D '%s %s=3D"%s"' % (result, key, value)

        return '%s />' % result


def cookId(id, title, file):
    if not id and hasattr(file,'filename'):
        filename=3Dfile.filename
        title=3Dtitle or filename
        id=3Dfilename[max(string.rfind(filename, '/'),
                        string.rfind(filename, '\\'),
                        string.rfind(filename, ':'),
                        )+1:]                 =20
    return id, title

class Pdata(Persistent, Implicit):
    # Wrapper for possibly large data

    next=3DNone
   =20
    def __init__(self, data):
        self.data=3Ddata

    def __getslice__(self, i, j):
        return self.data[i:j]

    def __len__(self):
        data =3D str(self)
        return len(data)

    def __str__(self):
        next=3Dself.next
        if next is None: return self.data

        r=3D[self.data]
        while next is not None:
            self=3Dnext
            r.append(self.data)
            next=3Dself.next
       =20
        return string.join(r,'')










------=_NextPart_000_0047_01C1982C.A9E2F190
Content-Type: application/octet-stream;
	name="HTTPRangeSupport.py"
Content-Transfer-Encoding: quoted-printable
Content-Disposition: attachment;
	filename="HTTPRangeSupport.py"

#########################################################################=
#####
#=20
# Zope Public License (ZPL) Version 1.0
# -------------------------------------
#=20
# Copyright (c) Digital Creations.  All rights reserved.
#=20
# This license has been certified as Open Source(tm).
#=20
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#=20
# 1. Redistributions in source code must retain the above copyright
#    notice, this list of conditions, and the following disclaimer.
#=20
# 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.
#=20
# 3. Digital Creations requests that attribution be given to Zope
#    in any manner possible. Zope includes a "Powered by Zope"
#    button that is installed by default. While it is not a license
#    violation to remove this button, it is requested that the
#    attribution remain. A significant investment has been put
#    into Zope, and this effort will continue if the Zope community
#    continues to grow. This is one way to assure that growth.
#=20
# 4. All advertising materials and documentation mentioning
#    features derived from or use of this software must display
#    the following acknowledgement:
#=20
#      "This product includes software developed by Digital Creations
#      for use in the Z Object Publishing Environment
#      (http://www.zope.org/)."
#=20
#    In the event that the product being advertised includes an
#    intact Zope distribution (with copyright and license included)
#    then this clause is waived.
#=20
# 5. Names associated with Zope or Digital Creations must not be used to
#    endorse or promote products derived from this software without
#    prior written permission from Digital Creations.
#=20
# 6. Modified redistributions of any form whatsoever must retain
#    the following acknowledgment:
#=20
#      "This product includes software developed by Digital Creations
#      for use in the Z Object Publishing Environment
#      (http://www.zope.org/)."
#=20
#    Intact (re-)distributions of any official Zope release do not
#    require an external acknowledgement.
#=20
# 7. Modifications are encouraged but must be packaged separately as
#    patches to official Zope releases.  Distributions that do not
#    clearly separate the patches from the original work must be clearly
#    labeled as unofficial distributions.  Modifications which do not
#    carry the name Zope may be packaged in any form, as long as they
#    conform to all of the clauses above.
#=20
#=20
# Disclaimer
#=20
#   THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
#   EXPRESSED 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 DIGITAL CREATIONS OR ITS
#   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.
#=20
#=20
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations.  Specific
# attributions are listed in the accompanying credits file.
#=20
#########################################################################=
#####
"""HTTP Range support utilities.

The RFC 2616 specification defines the 'Range' and 'If-Range' headers =
for
enabeling partial download of published resources. This module provides =
a
flag-interface and some support functions for implementing this =
functionality.

For an implementation example, see the File class in OFS/Image.py.
"""

__version__=3D'$Revision'[11:-2]

import re, string, sys
import Interface

WHITESPACE =3D re.compile('\s*', re.MULTILINE)

def parseRange(header):
    """RFC 2616 (HTTP 1.1) Range header parsing.

    Convert a range header to a list of slice indexes, returned as =
(start, end)
    tuples. If no end was given, end is None. Note that the RFC =
specifies the
    end offset to be inclusive, we return python convention indexes, =
where the
    end is exclusive. Syntactically incorrect headers are to be ignored, =
so if
    we encounter one we return None.

    """

    ranges =3D []
    add =3D ranges.append

    # First, clean out *all* whitespace. This is slightly more tolerant
    # than the spec asks for, but hey, it makes this function much =
easier.
    header =3D WHITESPACE.sub('', header)

    # A range header only can specify a byte range
    try: spec, sets =3D string.split(header, '=3D')
    except ValueError: return None
    if spec !=3D 'bytes':
        return None

    # The sets are delimited by commas.
    sets =3D string.split(sets, ',')
    # Filter out empty values, things like ',,' are allowed in the spec
    sets =3D filter(None, sets)
    # We need at least one set
    if not sets:
        return None

    for set in sets:
        try: start, end =3D string.split(set, '-')
        except ValueError: return None

        # Catch empty sets
        if not start and not end:
            return None

        # Convert to integers or None (which will raise errors if
        # non-integers were used (which is what we want)).
        try:
            if start =3D=3D '': start =3D None
            else: start =3D int(start)
            if end =3D=3D '': end =3D None
            else: end =3D int(end)
        except ValueError:
            return None

        # Special case: No start means the suffix format was used, which
        # means the end value is actually a negative start value.
        # Convert this by making it absolute.
        # A -0 range is converted to sys.maxint, which will result in a
        # Unsatisfiable response if no other ranges can by satisfied =
either.
        if start is None:
            start, end =3D -end, None
            if not start:
                start =3D sys.maxint
        elif end is not None:
            end =3D end + 1 # Make the end of the range exclusive

        if end is not None and end <=3D start:
            return None

        # And store
        add((start, end))

    return ranges

def optimizeRanges(ranges, size):
    """Optimize Range sets, given those sets and the length of the =
resource.

    Optimisation is done by first expanding relative start values and =
open
    ends, then sorting and combining overlapping ranges. We also remove
    unsatisfiable ranges (where the start lies beyond the size of the =
resource).

    """

    expanded =3D []
    add =3D expanded.append
    for start, end in ranges:
        if start < 0:
            start =3D size + start
        end =3D end or size
        if end > size: end =3D size
        # Only use satisfiable ranges
        if start < size:
            add((start, end))

    ranges =3D expanded
    ranges.sort()
    ranges.reverse()
    optimized =3D []
    add =3D optimized.append
    start, end =3D ranges.pop()
   =20
    while ranges:
        nextstart, nextend =3D ranges.pop()
        # If the next range overlaps
        if nextstart < end:
            # If it falls within the current range, discard
            if nextend <=3D end:
                continue
           =20
            # Overlap, adjust end
            end =3D nextend
        else:
            add((start, end))
            start, end =3D nextstart, nextend

    # Add the remaining optimized range
    add((start, end))
   =20
    return optimized

class HTTPRangeInterface(Interface.Base):
    """Objects implementing this Interface support the HTTP Range =
header.

    Objects implementing support for the HTTP Range header will return =
partial
    content as specified in RFC 2616. Note that the'If-Range' header =
must
    either be implemented correctly or result in a normal '200 OK' =
response at
    all times.

    This interface specifies no methods, as this functionality can =
either be
    implemented in the index_html or __call__ methods of a published =
object.

    """

------=_NextPart_000_0047_01C1982C.A9E2F190--