diff --git a/NEWS b/NEWS index 4e86718..8732ae9 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,8 @@ +1.4.5: June 18, 2008: + * You can now resize embedded images (small, medium, or large). + * Fixed a bug that potentially caused link pulldowns to open in the wrong + location when the page was scrolled past the top. + 1.4.4: June 17, 2008: * Links to embedded images now show up within the note tree's list of links. * Links to files that have not yet been uploaded (or have been deleted) are diff --git a/controller/Files.py b/controller/Files.py index 257b558..da0af52 100644 --- a/controller/Files.py +++ b/controller/Files.py @@ -333,15 +333,18 @@ class Files( object ): @grab_user_id @validate( file_id = Valid_id(), - user_id = Valid_id( none_okay = True ), + max_size = Valid_int( min = 10, max = 1000, none_okay = True ), + user_id = Valid_id( none_okay = True ) ) - def thumbnail( self, file_id, user_id = None ): + def thumbnail( self, file_id, max_size = None, user_id = None ): """ Return a thumbnail for a file that a user has previously uploaded. If a thumbnail cannot be generated for the given file, return a default thumbnail image. @type file_id: unicode @param file_id: id of the file to return a thumbnail for + @type max_size: int or NoneType + @param max_size: maximum thumbnail width or height in pixels (optional, defaults to a small size) @type user_id: unicode or NoneType @param user_id: id of current logged-in user (if any) @rtype: generator @@ -360,14 +363,17 @@ class Files( object ): cherrypy.response.headerMap[ u"Content-Type" ] = u"image/png" + DEFAULT_MAX_THUMBNAIL_SIZE = 125 + if not max_size: + max_size = DEFAULT_MAX_THUMBNAIL_SIZE + # attempt to open the file as an image image_buffer = None try: image = Upload_file.open_image( file_id ) # scale the image down into a thumbnail - THUMBNAIL_MAX_SIZE = ( 125, 125 ) # in pixels - image.thumbnail( THUMBNAIL_MAX_SIZE, Image.ANTIALIAS ) + image.thumbnail( ( max_size, max_size ), Image.ANTIALIAS ) # save the image into a memory buffer image_buffer = StringIO() diff --git a/controller/test/Test_files.py b/controller/test/Test_files.py index 95a69f9..b1abd9e 100644 --- a/controller/test/Test_files.py +++ b/controller/test/Test_files.py @@ -566,6 +566,82 @@ class Test_files( Test_controller ): assert image assert image.size == ( 125, 50 ) + def test_thumbnail_with_max_size( self ): + self.login() + + # make the test image big enough to require scaling down + image = Image.open( StringIO( self.IMAGE_DATA ) ) + image = image.transform( ( 250, 250 ), Image.QUAD, range( 8 ) ) + + image_data = StringIO() + image.save( image_data, "PNG" ) + + self.http_upload( + "/files/upload?file_id=%s" % self.file_id, + dict( + notebook_id = self.notebook.object_id, + note_id = self.note.object_id, + ), + filename = self.filename, + file_data = image_data.getvalue(), + content_type = self.content_type, + session_id = self.session_id, + ) + + max_size = 225 + result = self.http_get( + "/files/thumbnail?file_id=%s&max_size=%s" % ( self.file_id, max_size ), + session_id = self.session_id, + ) + + headers = result[ u"headers" ] + assert headers + assert headers[ u"Content-Type" ] == self.content_type + assert u"Content-Disposition" not in headers + + file_data = "".join( result[ u"body" ] ) + image = Image.open( StringIO( file_data ) ) + assert image + assert image.size == ( max_size, max_size ) + + def test_thumbnail_with_max_size_without_scaling( self ): + self.login() + + # make the test image big enough to require scaling down + image = Image.open( StringIO( self.IMAGE_DATA ) ) + image = image.transform( ( 250, 250 ), Image.QUAD, range( 8 ) ) + + image_data = StringIO() + image.save( image_data, "PNG" ) + + self.http_upload( + "/files/upload?file_id=%s" % self.file_id, + dict( + notebook_id = self.notebook.object_id, + note_id = self.note.object_id, + ), + filename = self.filename, + file_data = image_data.getvalue(), + content_type = self.content_type, + session_id = self.session_id, + ) + + max_size = 300 + result = self.http_get( + "/files/thumbnail?file_id=%s&max_size=%s" % ( self.file_id, max_size ), + session_id = self.session_id, + ) + + headers = result[ u"headers" ] + assert headers + assert headers[ u"Content-Type" ] == self.content_type + assert u"Content-Disposition" not in headers + + file_data = "".join( result[ u"body" ] ) + image = Image.open( StringIO( file_data ) ) + assert image + assert image.size == ( 250, 250 ) + def test_thumbnail_with_non_image( self ): self.login() @@ -620,6 +696,28 @@ class Test_files( Test_controller ): assert headers assert headers.get( "Location" ) == u"http:///login?after_login=%s" % urllib.quote( path ) + def test_thumbnail_with_invalid_max_size( self ): + self.login() + + self.http_upload( + "/files/upload?file_id=%s" % self.file_id, + dict( + notebook_id = self.notebook.object_id, + note_id = self.note.object_id, + ), + filename = self.filename, + file_data = self.file_data, # not a valid image + content_type = self.content_type, + session_id = self.session_id, + ) + + result = self.http_get( + "/files/thumbnail?file_id=%s&max_size=0" % self.file_id, + session_id = self.session_id, + ) + + assert u"max size" in result[ u"body" ][ 0 ] + def test_thumbnail_without_access( self ): self.login() diff --git a/static/js/Wiki.js b/static/js/Wiki.js index 4c52e64..9f1f3e7 100644 --- a/static/js/Wiki.js +++ b/static/js/Wiki.js @@ -2524,10 +2524,8 @@ function calculate_position( node, anchor, relative_to ) { position.x += relative_pos.x; position.y += relative_pos.y; - // Work around an IE "feature" in which an element within an iframe changes its absolute - // position based on how far the page is scrolled. - if ( /MSIE/.test( navigator.userAgent ) ) - position.y -= getElement( "html" ).scrollTop; + // adjust the vertical position based on how far the page has scrolled + position.y -= getElement( "html" ).scrollTop; } } @@ -2983,6 +2981,12 @@ Upload_pulldown.prototype.shutdown = function () { this.link.pulldown = null; } + +SMALL_MAX_IMAGE_SIZE = 125; +MEDIUM_MAX_IMAGE_SIZE = 300; +LARGE_MAX_IMAGE_SIZE = 500; + + function File_link_pulldown( wiki, notebook_id, invoker, editor, link, ephemeral ) { link.pulldown = this; this.link = link; @@ -3028,6 +3032,9 @@ function File_link_pulldown( wiki, notebook_id, invoker, editor, link, ephemeral // if the link is an image thumbnail link, update the contents of the file link pulldown accordingly var image = getFirstElementByTagAndClassName( "img", null, this.link ); var embed_attributes = { "type": "checkbox", "class": "pulldown_checkbox", "id": "embed_checkbox" }; + var small_size_attributes = { "type": "radio", "id": "small_size_radio", "name": "size", "value": "small" }; + var medium_size_attributes = { "type": "radio", "id": "medium_size_radio", "name": "size", "value": "medium" }; + var large_size_attributes = { "type": "radio", "id": "large_size_radio", "name": "size", "value": "large" }; var left_justify_attributes = { "type": "radio", "id": "left_justify_radio", "name": "justify", "value": "left" }; var center_justify_attributes = { "type": "radio", "id": "center_justify_radio", "name": "justify", "value": "center" }; var right_justify_attributes = { "type": "radio", "id": "right_justify_radio", "name": "justify", "value": "right" }; @@ -3036,17 +3043,30 @@ function File_link_pulldown( wiki, notebook_id, invoker, editor, link, ephemeral addElementClass( this.thumbnail_span, "undisplayed" ); embed_attributes[ "checked" ] = "true"; - if ( hasElementClass( image, "left_justified" ) ) - left_justify_attributes[ "checked" ] = "true"; - else if ( hasElementClass( image, "center_justified" ) ) + var src = parseQueryString( image.src.split( "?" ).pop() ); + var max_size = src[ "max_size" ]; + if ( max_size == LARGE_MAX_IMAGE_SIZE ) + large_size_attributes[ "checked" ] = "true"; + else if ( max_size == MEDIUM_MAX_IMAGE_SIZE ) + medium_size_attributes[ "checked" ] = "true"; + else + small_size_attributes[ "checked" ] = "true"; + + if ( hasElementClass( image, "center_justified" ) ) center_justify_attributes[ "checked" ] = "true"; else if ( hasElementClass( image, "right_justified" ) ) right_justify_attributes[ "checked" ] = "true"; + else + left_justify_attributes[ "checked" ] = "true"; } else { + small_size_attributes[ "checked" ] = "true"; left_justify_attributes[ "checked" ] = "true"; } this.embed_checkbox = createDOM( "input", embed_attributes ); + this.small_size_radio = createDOM( "input", small_size_attributes ); + this.medium_size_radio = createDOM( "input", medium_size_attributes ); + this.large_size_radio = createDOM( "input", large_size_attributes ); this.left_justify_radio = createDOM( "input", left_justify_attributes ); this.center_justify_radio = createDOM( "input", center_justify_attributes ); this.right_justify_radio = createDOM( "input", right_justify_attributes ); @@ -3055,9 +3075,22 @@ function File_link_pulldown( wiki, notebook_id, invoker, editor, link, ephemeral "show image within note" ); + var small_size_label = createDOM( "label", + { "for": "small_size_radio", "class": "radio_label", "title": "Display a small thumbnail of this image." }, + "small" + ); + var medium_size_label = createDOM( "label", + { "for": "medium_size_radio", "class": "radio_label", "title": "Display a medium thumbnail of this image." }, + "medium" + ); + var large_size_label = createDOM( "label", + { "for": "large_size_radio", "class": "radio_label", "title": "Display a large thumbnail of this image." }, + "large" + ); + var left_justify_label = createDOM( "label", { "for": "left_justify_radio", "class": "radio_label", "title": "Left justify this image within the note." }, - "left justify" + "left" ); var center_justify_label = createDOM( "label", { "for": "center_justify_radio", "class": "radio_label", "title": "Center this image horizontally within the note." }, @@ -3065,13 +3098,20 @@ function File_link_pulldown( wiki, notebook_id, invoker, editor, link, ephemeral ); var right_justify_label = createDOM( "label", { "for": "right_justify_radio", "class": "radio_label", "title": "Right justify this image within the note." }, - "right justify" + "right" ); - this.image_justify_area = createDOM( "div", { "class": "undisplayed" }, - createDOM( "table" , { "id": "justify_table" }, + this.image_settings_area = createDOM( "div", { "class": "undisplayed" }, + createDOM( "table" , { "id": "image_settings_table" }, createDOM( "tbody", {}, createDOM( "tr", {}, + createDOM( "td", { "class": "field_label" }, "size: " ), + createDOM( "td", {}, this.small_size_radio, small_size_label ), + createDOM( "td", {}, this.medium_size_radio, medium_size_label ), + createDOM( "td", {}, this.large_size_radio, large_size_label ) + ), + createDOM( "tr", {}, + createDOM( "td", { "class": "field_label" }, "position: " ), createDOM( "td", {}, this.left_justify_radio, left_justify_label ), createDOM( "td", {}, this.center_justify_radio, center_justify_label ), createDOM( "td", {}, this.right_justify_radio, right_justify_label ) @@ -3081,7 +3121,7 @@ function File_link_pulldown( wiki, notebook_id, invoker, editor, link, ephemeral ); if ( image ) - removeElementClass( this.image_justify_area, "undisplayed" ); + removeElementClass( this.image_settings_area, "undisplayed" ); appendChildNodes( this.div, createDOM( "span", { "class": "field_label" }, "filename: " ) ); appendChildNodes( this.div, this.filename_field ); @@ -3089,7 +3129,7 @@ function File_link_pulldown( wiki, notebook_id, invoker, editor, link, ephemeral appendChildNodes( this.div, " " ); appendChildNodes( this.div, this.delete_button ); appendChildNodes( this.div, createDOM( "div", {}, this.embed_checkbox, embed_label ) ); - appendChildNodes( this.div, this.image_justify_area ); + appendChildNodes( this.div, this.image_settings_area ); // get the file's name and size from the server this.invoker.invoke( @@ -3109,6 +3149,9 @@ function File_link_pulldown( wiki, notebook_id, invoker, editor, link, ephemeral connect( this.delete_button, "onclick", function ( event ) { self.delete_button_clicked( event ); } ); connect( this.embed_checkbox, "onclick", function ( event ) { self.embed_clicked( event ); } ); + connect( this.small_size_radio, "onclick", function ( event ) { self.resize_image( event, "small" ); } ); + connect( this.medium_size_radio, "onclick", function ( event ) { self.resize_image( event, "medium" ); } ); + connect( this.large_size_radio, "onclick", function ( event ) { self.resize_image( event, "large" ); } ); connect( this.left_justify_radio, "onclick", function ( event ) { self.justify_image( event, "left" ); } ); connect( this.center_justify_radio, "onclick", function ( event ) { self.justify_image( event, "center" ); } ); connect( this.right_justify_radio, "onclick", function ( event ) { self.justify_image( event, "right" ); } ); @@ -3180,17 +3223,18 @@ File_link_pulldown.prototype.delete_button_clicked = function ( event ) { File_link_pulldown.prototype.embed_clicked = function ( event ) { if ( this.embed_checkbox.checked ) { - var image = createDOM( "img", { "src": "/files/thumbnail?file_id=" + this.file_id, "class": "left_justified" } ); + var image = createDOM( "img", { "src": "/files/thumbnail?file_id=" + this.file_id + "&max_size=" + SMALL_MAX_IMAGE_SIZE, "class": "left_justified" } ); var image_span = createDOM( "span", {}, image ); this.link_title = link_title( this.link ); this.link.innerHTML = image_span.innerHTML; addElementClass( this.thumbnail_span, "undisplayed" ); - removeElementClass( this.image_justify_area, "undisplayed" ); + removeElementClass( this.image_settings_area, "undisplayed" ); } else { this.justify_image( "left" ); this.left_justify_radio.checked = true; + this.small_size_radio.checked = true; removeElementClass( this.thumbnail_span, "undisplayed" ); - addElementClass( this.image_justify_area, "undisplayed" ); + addElementClass( this.image_settings_area, "undisplayed" ); this.link.innerHTML = this.link_title || this.filename_field.value || this.previous_filename; } @@ -3198,6 +3242,30 @@ File_link_pulldown.prototype.embed_clicked = function ( event ) { this.editor.resize(); } +File_link_pulldown.prototype.resize_image = function ( event, position ) { + var image = getFirstElementByTagAndClassName( "img", null, this.link ); + if ( !image ) + return; + + if ( position == "large" ) { + var max_size = LARGE_MAX_IMAGE_SIZE; + } else if ( position == "medium" ) { + var max_size = MEDIUM_MAX_IMAGE_SIZE; + } else { + var max_size = SMALL_MAX_IMAGE_SIZE; + } + + // when the newly resized image finishes loading, update the pulldown position and resize the + // editor + var self = this; + connect( image, "onload", function () { + self.update_position( self.link, self.editor.iframe ); + self.editor.resize(); + } ); + + image.setAttribute( "src", "/files/thumbnail?file_id=" + this.file_id + "&max_size=" + max_size ); +} + File_link_pulldown.prototype.justify_image = function ( event, position ) { var image = getFirstElementByTagAndClassName( "img", null, this.link ); if ( !image ) diff --git a/view/Toolbar.py b/view/Toolbar.py index bf807de..8ceae44 100644 --- a/view/Toolbar.py +++ b/view/Toolbar.py @@ -23,7 +23,7 @@ class Toolbar( Div ): ) ), Div( Input( type = u"image", - id = u"attachFile", title = u"attach file", + id = u"attachFile", title = u"attach file or image", src = u"/static/images/toolbar/attach_button.png", width = u"40", height = u"40", class_ = "image_button",