内容管理系统中处理图像,第二部分






4.90/5 (68投票s)
2006年2月22日
14分钟阅读

149760

1109
基于浏览器的图像缩放和优化。
引言
这是我的文章“内容管理系统中处理图像”的第二部分。请在开始阅读本部分之前阅读文章的第一部分。
生成画布
无论用户是上传了新的原始图像还是选择了缩略图,我们现在都已将有关图像宽度、高度以及服务器 ID(新生成或从缩略图中推断的)的信息提供给了控件。
如果我们处于uploadButton_Click
中,新的缩略图将被添加到会话中的缩略图名称列表中。
SessionImages.Add(thumbnailFileName);
然后,uploadButton_Click
和btnThumb_Command
都会调用UseRaw()
。此方法首先检查原始图像的尺寸是否与控件的ImageWidth
和ImageHeight
相同,如果是,则假定用户已上传了可用于 Web 的图像。在这种情况下,将调用IImageProvider
的SaveRawImageAsWebImage
,它会有效地将原始图像复制到 Web 图像目录,仅在上传的格式与指定的格式不同时进行任何图形重绘。然后,控件的模式将设置为ControlMode.Changed
,其工作就完成了。
通常,图像的尺寸不正确,因此控件会调用CanvasFromRaw()
来生成用户可以在其上进行选择的画布。
private void CanvasFromRaw()
{
// we're going to need to do some further work with this image
// so let's work out a few things about it
aspectRatio = (float)rawWidth / (float)rawHeight;
// the image is not the right size so we need to make a canvas
string[] clientDimensions = hiddenField.Value.Split(new char[] { ',' });
// we'll allow the canvas to be up to 80%
// of the user's current browser window size:
int clientX = Convert.ToInt32(clientDimensions[0]) * 4 / 5;
int clientY = Convert.ToInt32(clientDimensions[1]) * 4 / 5;
float clientRatio = (float)clientX / (float)clientY;
// which is the dimension that should constrain the canvas size?
if (clientRatio > aspectRatio)
{
// y axis constrains the canvas size
canvasHeight = clientY;
canvasWidth = rawWidth * clientY / rawHeight;
}
else
{
canvasWidth = clientX;
canvasHeight = rawHeight * clientX / rawWidth;
}
canvasImageName = ImageProvider.CreateCanvas(
webImageFormat, webImageQuality,
canvasWidth, canvasHeight);
canvas.Src = getImageSource(canvasImageName, "canvas");
canvas.Width = canvasWidth;
canvas.Height = canvasHeight;
controlMode = ControlMode.Canvas;
}
在这里,我们利用了我们之前捕获的视口尺寸。我们计算上传图像的纵横比。
aspectRatio = (float)rawWidth / (float)rawHeight;
以及浏览器视口的纵横比。
float clientRatio = (float)clientX / (float)clientY;
比较它们可以让我们确定哪个维度应该约束画布,使其适合用户浏览器。我们还将客户端尺寸乘以 4/5,以便画布周围有一些空间,不会一直延伸到浏览器的边缘,那样会很难看,而且没有额外的 UI 空间。
与之前的缩略图示例相比,调用IImageProvider.CreateCanvas
更直接。
public string CreateCanvas(WebImageFormat format,
WebImageQuality quality, int canvasWidth,
int canvasHeight)
{
purge(WebImageMaker.CanvasImageDirName);
string canvasFileName = null;
using (Image rawImg = Image.FromFile(getRawFilePath()))
{
using (Bitmap canvas = new Bitmap(
canvasWidth, canvasHeight, PixelFormat.Format24bppRgb))
{
using (Graphics g = Graphics.FromImage(canvas))
{
setGraphicsQuality(g, quality);
g.DrawImage(rawImg, 0, 0, canvasWidth, canvasHeight);
string filePath = getFilePath(
WebImageMaker.CanvasImageDirName, format);
canvas.Save(filePath, getGDIFormat(format));
canvasFileName = Path.GetFileName(filePath);
}
}
}
return canvasFileName;
}
此处使用的Graphics.DrawImage
重载只是将源图像绘制到整个画布上,并进行适当的缩放。setGraphicsQuality
、getFilePath
和getGDIFormat
的调用与缩略图情况相同。
回到控件中,我们使用以下方式设置画布图像的源:
getImageSource(canvasImageName, "canvas");
此方法会根据是使用处理程序还是控件本身提供图像来适当地生成图像 URL。
我们现在处于ControlMode.Canvas
模式,并且当调用Render
方法时,控件将渲染画布 UI。
插入客户端脚本引用
我们在OnPreRender
阶段确保客户端 UI 包含正确的 JavaScript。此方法中的条件编译已在前面讨论过。最终结果通常是引用WebImageMaker_normal.js。它包含用于定位缩略图选择器及其可见性(特别是当页面上可能存在多个控件实例时)的脚本,以及前面讨论过的setViewportDimensions
函数。当控件处于Canvas
模式时,将改用WebImageMaker_canvas.js。我们还需要注入一段额外的 JavaScript 来初始化浏览器中的画布。
Page.ClientScript.RegisterStartupScript(
this.GetType(), this.ClientID, getInitScript(), true);
private string getInitScript()
{
string s = @"
function init{0}()
{{
initialise('{1}', '{2}', '{3}', '{4}', '{5}',
'{6}', '{7}', '{8}');
}}
window.onload = init{0};
";
return String.Format(s, this.ClientID,
popupDiv.ClientID, canvas.ClientID,
selectionBox.ClientID, imageWidth,
imageHeight, targetImage.ClientID,
confirmSelection.ClientID,
lblDebugInfo.ClientID);
}
(当使用String.Format
时,双括号“{{”用于转义单个“{”。)
客户端画布脚本
需要记住的关键点是,用户的选择只是一个带有虚线边框的透明DIV
元素,并且它被绝对定位,看起来像是浮在画布图像上。
画布脚本的职责是:
- 将所有元素设置在页面上的正确位置。
- 以合适的默认位置和大小初始化选择
DIV
,如果控件中指定了ImageWidth
和ImageHeight
,则保持正确的aspectRatio
。 - 响应用户的鼠标移动和点击,以便:
- 当鼠标指针靠近选择的角落或边缘时,光标会变成一个调整大小的图标,指示可以沿相应方向进行调整。
- 当鼠标指针在选择内部且远离边缘时,光标会变成一个移动图标。
- 当鼠标指针靠近选择的角落或边缘时,单击并拖动会以直观自然的方式调整选择大小。
- 当鼠标指针在选择内部时,单击并拖动应会移动选择。
- 如果用户调整了选择的大小并且正在强制执行
aspectRatio
,脚本应确保选择保持正确的纵横比,即使用户的鼠标移动否则会改变该比例。这种对选择的约束在图像编辑包中很常见——脚本应确保用户在拖动边缘进行调整大小时,选择的行为符合直觉。
- 在页面重新提交之前,存储选择相对于画布的位置和大小。
页面加载时,将调用initialise(..)
方法,并带有控件在前面描述的预渲染阶段写出的参数值。
首先,所有需要操作的 HTML 元素的引用都存储在变量中,以便于使用。
...
oCanvas = document.getElementById(canvasID);
oSelection = document.getElementById(selectionBoxID);
...
然后,我们将popupDiv
元素的大小和位置调整为比画布图像稍宽,并从视口的顶部和左侧缩进。我们还存储了画布本身的位置和大小以备后用。
canvasRect = rectangle(oCanvas);
rectangle
函数需要一些解释。它用于表示元素的位置和大小,而无需不断访问其样式属性(例如,oElement.style.left
)。它还允许我们捕获可能正在更改的元素的快照。这在处理用户的选择时特别有用。而不是不断调整选择DIV
的大小和位置,我们使用一个更抽象的对象来表示选择(或任何其他元素)的尺寸和位置。
// returns an object that represents
// the size and position of the element oBlock
function rectangle(oBlock)
{
return {
x: parseInt(oBlock.style.left),
y: parseInt(oBlock.style.top),
w: parseInt(oBlock.style.width),
h: parseInt(oBlock.style.height)
};
}
此方法返回一个具有w
、h
、x
和y
属性的对象。它类似于我们之前使用的System.Drawing.Rectangle
。在一系列对选择的操作开始时,我们可以获取一个新的矩形对象来表示当前选择的快照。然后,我们可以通过更改其x
、y
、w
和h
属性来缩放、移动和约束此表示,完成后,调用setSelection
并传入修改后的矩形,将新的位置和位置应用到选择DIV
。
// applies the size and position information in the
// rectangle rect to the selection div
function setSelection(rect)
{
oSelection.style.left = rect.x + "px";
oSelection.style.top = rect.y + "px";
oSelection.style.width = rect.w + "px";
oSelection.style.height = rect.h + "px";
}
这意味着我们不会不断更改选择元素的样式属性本身,这在调整大小时可能会使问题复杂化。
回到initialise(..)
函数,我们继续通过适当地定位和调整选择的大小来设置页面。
if(iReqdWidth > 0 && iReqdHeight > 0)
{
bConstrain = true;
aspectRatio = iReqdWidth / iReqdHeight;
}
bConstrain
是一个全局变量,在用户调整选择大小时进行检查,以确定是否应“微调”选择以确保保持正确的aspectRatio
。这演示了rectangle
的使用。
var selection = rectangle(oSelection);
constrain(selection);
setSelection(selection);
我们将选择的快照存储在变量selection
中,然后调用constrain(..)
,然后将矩形的尺寸重新应用到“真实”选择。
constrain(..)
函数稍后将进行描述。
为了完成初始化,我们挂接了一些鼠标事件的事件处理程序。
// now hook up some event handling:
document.onmousemove = move;
document.onmouseup = up;
document.onmousedown = down;
oCanvas.ondrag = function(){return false;}
oSelection.ondrag = function(){return false;}
document.ondrag = function(){return false;}
我们需要将鼠标事件处理程序放在整个文档上,而不是仅放在选择或画布上,因为在拖动或调整大小时,用户可能会将鼠标移出画布甚至popupDiv
,这属于正常使用情况。当强制执行纵横比并约束图像大小时,尤其如此,因为用户最初抓住的选择点可能无法与脚本约束选择尺寸时的用户鼠标同步。最后三个事件处理程序对于取消任何拖动事件至关重要。否则,浏览器可能会通过选择用户在移动或调整选择大小时鼠标经过的页面部分来处理这些事件。这将产生非常混乱的用户体验。
移动鼠标
当用户移动鼠标时,将调用move(e)
函数。两个全局变量bMoving
和bResizing
用于跟踪脚本是决定用户正在移动还是正在调整选择大小。它们将在响应MouseDown
事件时设置。最初,两者都为false
,表示用户只是在移动鼠标。move
函数的大部分是一个三条件if
语句,其中最后一个是“只是移动鼠标”的条件。我们稍后会回到前两个,它们处理移动和调整大小。
...
else
{
resizeXMode = "";
resizeYMode = "";
var targetSize = 15;
if(p.x >= selection.x && p.x <= (selection.x + selection.w)
&& p.y >= selection.y && p.y <= (selection.y + selection.h))
{
// cursor is inside the selection
// default to move behaviour
oSelection.style.cursor = "move";
oCanvas.style.cursor = "move";
bCanMove = true;
// further checks for hotspots omitted...
...
两个全局变量resizeXMode
和resizeYMode
用于跟踪根据当前鼠标位置允许用户执行哪种类型的调整大小。它们独立变化。resizeXMode
变量可以是“E
”(东)或“W
”(西),resizeYMode
可以是“N
”(北)或“S
”(南)。当用户在else
块中移动鼠标时,我们将设置这两个变量。
它们在块的开头都被清空,然后我们检查鼠标指针是否在选择内部。如果是,我们将bCanMove
设置为true
,表示用户可以从该点开始移动操作。我们还将光标设置为移动符号。然后,我们进一步检查用户是否在八个“热点”区域之一,可以从中启动调整大小操作,如果是,则相应地设置resizeXMode
和resizeYMode
变量。我们还将光标符号设置为指向适当的方向。这些热点的大小由targetSize
变量控制,此处设置为 15 像素。

此条件其余部分是大量繁琐的代码,用于找出光标在哪个热点上,并相应地设置resizeXMode
和resizeYMode
变量以及光标图标。
鼠标抬起和鼠标按下
down(e)
和up()
函数处理鼠标按下和释放。在整个脚本中,我们大量使用鼠标移动和鼠标按下事件发生的位置。为了更容易地对其进行编码,我们使用一个函数,该函数返回一个表示事件发生位置的对象。
// returns an object that represents the position relative to
// popupOrigin that event e occurred.
function point(e)
{
if (e.pageX || e.pageY)
{
this.x = e.pageX;
this.y = e.pageY;
}
else if (e.clientX || e.clientY)
{
// need to use both document.body and document.documentElement to
// cater for various combinations of IE 5/6 in normal and quirksmode
this.x = e.clientX + document.body.scrollLeft
+ document.documentElement.scrollLeft;
this.y = e.clientY + document.body.scrollTop
+ document.documentElement.scrollTop;
}
// give the point position relative to the popup
this.x -= popupOrigin.x;
this.y -= popupOrigin.y;
}
这可以在下面的down(e)
事件处理程序中看到。请注意,第一行允许以跨浏览器方式处理事件,如quirksmode上所述。
function down(e)
{
if (!e) var e = window.event;
downPoint = new point(e);
originalRect = rectangle(oSelection);
if(bCanMove && resizeXMode == "" && resizeYMode == "")
{
bMoving = true;
}
if(resizeXMode != "" || resizeYMode != "")
{
bResizing = true;
}
return false;
}
function up()
{
bMoving = false;
bResizing = false;
}
当用户按下并按住鼠标按钮时,我们首先存储按下事件发生时的鼠标位置。
downPoint = new point(e);
稍后将使用此值来确定鼠标移动了多远。我们还将选择的大小和位置作为矩形存储在originalRect
变量中——同样,稍后将使用此变量来确定要调整选择的大小或移动多少。
如果鼠标在选择上方但不在热点区域(resizeXMode == "" && resizeYMode == ""
),则我们将bMoving
设置为true
。下次在响应进一步鼠标移动时调用 move 函数时,我们将进入“if
”语句中的第一个条件。但是,如果鼠标指针位于热点区域,则我们将全局变量bResizing
设置为true
。下次在响应用户鼠标移动时调用 move 函数时,将调用其“if
”语句中的第二个条件。这种情况一直持续到用户停止按住鼠标按钮。up 事件处理程序不能再简单了——它只是将两个全局变量bMoving
和bResizing
设置为false
,以便进一步的鼠标移动默认到前面讨论的“只是移动鼠标”状态。
移动实际上是最简单的状态
function move(e)
{
if(bInMove) return; // we're already processing a mousemove event
bInMove = true;
oCanvas.style.cursor = "auto";
oSelection.style.cursor = "auto";
bCanMove = false;
if (!e) var e = window.event;
var p = new point(e);
var selection = rectangle(oSelection);
if(bMoving)
{
var dx = p.x - downPoint.x;
var dy = p.y - downPoint.y;
selection.x = 0 + originalRect.x + dx;
selection.y = 0 + originalRect.y + dy;
setSelection(selection);
checkConfine(selection);
}
...
如前所述,我们捕获了当前选择大小和位置的快照。然后,我们找出自用户开始移动以来鼠标移动了多远(在down(e)
中),并将差值存储在dx
和dy
中。然后,我们所做的就是将新的选择位置设置为旧位置加上差值,然后调用setSelection
将新的选择矩形应用到选择DIV
。然后我们调用checkConfine(..)
,它会在选择不在画布内的任何位置时禁用 OK 按钮并显示警告消息。
function checkConfine(rect)
{
var msg = "";
if((rect.x) < canvasRect.x)
msg += "Selection extends to the left of the canvas. ";
if(rect.y < canvasRect.y)
msg += "Selection extends above canvas. ";
if((rect.x + rect.w) > (canvasRect.x + canvasRect.w))
msg += "Selection extends to the right of the canvas. ";
if((rect.y + rect.h) > (canvasRect.y + canvasRect.h))
msg += "Selection extends below the canvas. ";
if(msg)
{
oConfirmButton.disabled = true;
oDebugInfo.innerHTML = "WARNING: " + msg;
}
else
{
oConfirmButton.disabled = false;
oDebugInfo.innerHTML = "";
}
}
调整大小
move 函数的if
语句中的中间条件处理调整大小。通过向右移动右边缘或向下移动底部边缘来更改选择大小相对简单,因为您只需将选择DIV
的宽度或高度增加用户移动鼠标的量即可。给人的感觉是向上移动选择的顶部或向左移动选择的左侧更复杂。在这些情况下,您希望底部或右边缘保持不变,因此您需要同时将选择的大小增加用户移动鼠标的量,并将其向上或向左移动相同的量。这使得用户感觉自然,并且是他们期望的。
...
else if(bResizing)
{
var dx = p.x - downPoint.x;
var dy = p.y - downPoint.y;
if(resizeXMode == "E")
{
selection.w = 0 + originalRect.w + dx;
if(selection.w < minimumSelectionSize)
{
selection.w = minimumSelectionSize;
bResizing = false;
}
}
if(resizeXMode == "W")
{
selection.w = 0 + originalRect.w - dx;
selection.x = 0 + originalRect.x + dx;
if(selection.w < minimumSelectionSize)
{
dx = selection.w - minimumSelectionSize;
selection.w = minimumSelectionSize;
selection.x += dx;
bResizing = false;
}
}
if(resizeYMode == "S")
{
selection.h = 0 + originalRect.h + dy;
if(selection.h < minimumSelectionSize)
{
selection.h = minimumSelectionSize;
bResizing = false;
}
}
if(resizeYMode == "N")
{
selection.h = 0 + originalRect.h - dy;
selection.y = 0 + originalRect.y + dy;
if(selection.h < minimumSelectionSize)
{
dy = selection.h - minimumSelectionSize;
selection.h = minimumSelectionSize;
selection.y += dy;
bResizing = false;
}
}
constrain(selection);
setSelection(selection);
checkConfine(selection);
}
...
每个方向都是单独处理的。如果用户抓住了角热点,则会满足四个条件中的两个。与“E”或“S”的移动相比,“W”或“N”的移动更复杂,如上所述。如果选择太小,我们也会取消调整大小的操作(通过将bResizing
设置为false
)(此脚本使用默认的最小选择大小为 40 像素的正方形,这足以仍然抓住所有热点并移动选择)。
一旦我们根据用户鼠标移动为选择赋予了新的形状,我们就需要将其约束到所需纵横比(如果正在强制执行)
function constrain(rect)
{
if(bConstrain && rect)
{
var newRatio = rect.w / rect.h;
if(newRatio > aspectRatio)
{
// it's too "landscapey" -
// keep the height the same but reduce the width
// in accordance with the required aspectRatio
var correctWidth = Math.round(aspectRatio * rect.h);
if(correctWidth >= minimumSelectionSize)
{
if(resizeXMode == "W")
{
var rightPos = rect.x + rect.w;
rect.x = rightPos - correctWidth;
}
rect.w = correctWidth;
}
else
{
// the constrained selection will be too small
rect.w = minimumSelectionSize;
var newH = Math.round(minimumSelectionSize / aspectRatio);
var dy = newH - rect.h;
rect.h = newH;
if(resizeYMode == "N")
{
rect.y -= dy;
}
}
}
else
{
// it's too "portraity" -
//keep the width the same but reduce the height
// in accordance with the required aspectRatio
var correctHeight = Math.round(rect.w / aspectRatio);
if(correctHeight >= minimumSelectionSize)
{
if(resizeYMode == "N")
{
var bottomPos = rect.y + rect.h;
rect.y = bottomPos - correctHeight;
}
rect.h = correctHeight;
}
else
{
// the constrained selection will be too small
rect.h = minimumSelectionSize;
var newW = Math.round(minimumSelectionSize * aspectRatio);
var dx = newW - rect.w;
rect.w = newW;
if(resizeXMode == "W")
{
rect.x -= dx;
}
}
}
}
}
首先,我们必须决定当前纵横比是比所需纵横比更宽(更风景化)还是更高(更肖像化)。这告诉我们哪个维度可以保持不变,哪个维度需要修改才能使整体形状恢复正常。同样,如果我们向左或向上调整大小,则必须保持右边缘或底部边缘的可见位置不变。我们还需要确保在约束一个维度(这总是会减小尺寸)时,我们没有将其低于minimumSelectionSize
。如果是这样,我们需要将该维度设置为最小值,并增加另一个维度以进行补偿。
此 constrain 函数有多种工作方式——例如,它可以反过来工作,总是增加另一个维度而不是减小它。然而,这一系列规则在实际操作选择时似乎能产生最直观的结果。
创建最终的 Web 图像
按下 OK 按钮会导致当前选择信息被写入一个隐藏的表单字段。
function storeSelectionInfo(hiddenFieldID)
{
var field = document.getElementById(hiddenFieldID);
// get the selection dimensions relative to the canvas - x,y,w,h
var selection = rectangle(oSelection);
field.value = (selection.x - canvasRect.x) + ","
+ (selection.y - canvasRect.y) + ","
+ selection.w + "," + selection.h;
}
然后表单发布回服务器。这会将我们带入 OK 按钮的服务器端事件处理程序。
void confirmSelection_Click(object sender, EventArgs e)
{
string[] clientDimensions = hiddenField.Value.Split(new char[] { ',' });
int x = Convert.ToInt32(clientDimensions[0]);
int y = Convert.ToInt32(clientDimensions[1]);
int w = Convert.ToInt32(clientDimensions[2]);
int h = Convert.ToInt32(clientDimensions[3]);
// now we have the x,y,w,h of the selection relative to the canvas, so
// we have to scale the selection so that it is a selection from the
// original raw image:
float scaleFactor = (float)canvasWidth / (float)rawWidth;
Rectangle transformedSelection = new Rectangle(
(int)(x / scaleFactor),
(int)(y / scaleFactor),
(int)(w / scaleFactor),
(int)(h / scaleFactor));
// transformedSelection now represents the user's selected crop on the
// raw image rather than the canvas image
// now determine what the dimensions of the final image should be. If
// ImageWidth and ImageHeight were both set then we already know, but
// if one of them was "*" then we need to work out what it should
// proportionally be from the user's selected crop shape:
float selectionAspectRatio = (float)w / (float)h;
int reqdWidth = intImageWidth;
int reqdHeight = intImageHeight;
// these should never be both <= 0
if (reqdWidth <= 0)
{
reqdWidth = (int)(selectionAspectRatio * reqdHeight);
}
else if (reqdHeight <= 0)
{
reqdHeight = (int)(reqdWidth / selectionAspectRatio);
}
// now we have everything we need: what area (transformedSelection) to crop
// out of the raw image, and what dimensions this cropped area should be
// resized to:
webImageName = ImageProvider.CropAndScale(
transformedSelection,
webImageFormat, webImageQuality,
reqdWidth, reqdHeight);
targetImage.Src = getImageSource(webImageName, "web");
controlMode = ControlMode.Changed;
}
使用来自客户端的坐标,我们创建一个新的System.Drawing.Rectangle
,称为transformedSelection
,它表示原始图像坐标系中画布上用户的选择。然后,如果reqdWidth
和reqdHeight
中的只有一个已设置为控件的属性(即,求值为正整数而不是“*”),我们就需要弄清楚最终 Web 图像的另一个尺寸应该是多少。我们使用选择的纵横比来确定这一点。一旦我们拥有了所有这些信息,我们就可以调用IImageProvider
的CropAndScale
方法。
public string CropAndScale(System.Drawing.Rectangle
transformedSelection, WebImageFormat format,
WebImageQuality quality, int reqdWidth, int reqdHeight)
{
string webFileName = null;
Rectangle dest = new Rectangle(0, 0, reqdWidth, reqdHeight);
using (Image rawImg = Image.FromFile(getRawFilePath()))
{
using (Bitmap webImage = new Bitmap(
reqdWidth, reqdHeight, PixelFormat.Format24bppRgb))
{
using (Graphics g = Graphics.FromImage(webImage))
{
setGraphicsQuality(g, quality);
g.DrawImage(
rawImg, dest, transformedSelection, GraphicsUnit.Pixel);
string filePath = getFilePath(
WebImageMaker.WebImageDirName, format);
webImage.Save(filePath, getGDIFormat(format));
webFileName = Path.GetFileName(filePath);
}
}
}
return webFileName;
}
这与我们之前看到的 GDI+ 代码非常相似。我们创建一个新的矩形来表示最终的 Web 图像(dest
),然后将转换后的选择绘制到它上面。
g.DrawImage(rawImg, dest, transformedSelection, GraphicsUnit.Pixel);
然后,我们将控件的图像设置为此新的 Web 图像,并将controlMode
更改为ControlMode.Changed
。开发人员可以通过访问其WebImagePath
属性来查询控件以获取新 Web 图像的文件路径。
可能的额外功能
此控件的功能可以有很多添加方式。一个例子是提供一个 Web 图像创建阶段的钩子,允许外部代码在保存图像之前对其进行处理,例如添加边框或版权声明。
AJAX
此代码中有几个地方可以使用一些脚本回调来改善用户体验。然而,这个控件的“房间里的大象”是需要将原始文件传回服务器,这很可能是所有 postback 中的首要问题。在解决了这个问题之后,节省其他地方的几个 postback 似乎微不足道。
历史
- 2006 年 2 月 22 日:初次发布
- 2009 年 2 月 17 日:更新演示项目