65.9K
CodeProject 正在变化。 阅读更多。
Home

内容管理系统中图片的处理,第一部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (122投票s)

2006年2月22日

CPOL

23分钟阅读

viewsIcon

437524

downloadIcon

3166

基于浏览器的图像缩放和优化。

引言

大多数基于网络的內容管理系统提供各种工具来帮助贡献者输入文本。当涉及到图形时,内容贡献者通常被期望向系统提供“Web就绪”图像。这意味着编辑用户需要了解图像优化和Web图像格式,或者需要额外的工作人员从原始材料中制作Web就绪图像。本文演示了此问题的技术解决方案。

原始图像可能来自数码相机、手机或扫描件。这些图像的像素尺寸和文件大小几乎总是过大,无法用作Web图像。在典型场景中,完成的图像需要被限制在特定的像素大小,以适应网站使用的任何模板。例如,如果我们查看 BBC 新闻 网站上的新闻报道,我们可以看到内容图像始终是203像素宽。当它们出现在首页时,它们也必须是152像素高,但当它们出现在新闻报道的文本旁边时,图片的高度可以变化以适应内容。缩略图是66像素的正方形。我们需要某种形式的用户界面,将“原始”图像作为输入,并允许非技术用户生成一个大小适当的图像,该图像既美观,又针对Web进行了优化,并符合我们可能希望施加的任何任意图像尺寸规则。几乎总是,通过一些精心的裁剪,原始图像的Web使用效果会得到改善。

许多内容管理系统提供一定程度的图像处理自动化,通常仅限于从提供的文件中自动生成缩略图。虽然这种方法对于显示一批图像包含什么很有用,但它很少能生成令人满意的“预告”缩略图,就像你在网站首页上找到的那样。这种缩略图需要人工干预。

解决方案是一个ASP.NET服务器控件,允许最终用户从本地文件系统上传图像,并进行裁剪、缩放和优化,以获得满意的Web图像。该服务器控件继承自新的CompositeControl类,这使得从现有、更简单的控件中组合新的服务器控件变得更容易。此控件在客户端大量使用JavaScript、DOM和CSS来驱动用户界面,并需要现代Web浏览器。它将在所有平台上的Firefox、Windows上的IE6以及MacOSX上的Safari中工作。该控件在aspx页面中使用时如下所示:

<guild:WebImageMaker id="wim_main" runat="server" width="250" 
    BorderStyle="Solid" BorderColor="#c0c0c0" BorderWidth="1px" 
    CancelButtonText="Cancel" ConfirmButtonText="OK"
    UploadButtonText="upload image..." 
    ImageWidth="203" ImageHeight="*"
    ImageUrl=" " 
    WorkingDirectory="C:\imagemaker_workingdir" 
    Format="jpg" Quality="High"  />

这里的关键属性是最后六个。`ImageWidth` 和 `ImageHeight` 决定了控件将创建的 Web 图像的尺寸。这些属性实际上是字符串,允许我们输入 "*" 字符,表示我们不关心其中一个尺寸是什么。这两个属性中至少有一个必须计算为正整数,否则控件没有足够的信息来确定最终图像的大小。`ImageUrl` 用于允许控件渲染当前 Web 图像,如果它用于更改现有 Web 图像而不是创建新图像。`WorkingDirectory` 指示控件将保存所创建图像的位置。在成功运行此项目之前,您需要创建此目录并授予 ASP.NET 正在使用的进程对此目录的写入权限。工作目录不需要在 Web 根目录下。`Format` 和 `Quality` 决定了控件将如何生成最终的 Web 图像。

该控件在其“默认”条件下如下所示。尚未上传图像。如果用户正在更改现有图像,则现有Web图像(ImageUrl属性)将在此处可见,而不是默认的占位符图形。

Control in default state

文件选择器是一个标准的WebControls.FileUpload控件。用户浏览文件系统以查找要上传的图像文件,然后单击“上传文件...”按钮。对于大型图像,这可能需要一段时间。一旦表单发布,控件的服务器端代码会检查文件以确保它是图像。它还会存储原始图像尺寸以供后续使用。

为了让用户裁剪图像,控件在文件上传后必须将图像重新渲染回浏览器,以便用户拥有一个“画布”来操作。然而,上传的图像通常过大,无法适应用户的浏览器窗口,因此控件需要在将图像渲染回作为画布之前对其进行缩放。这个缩放操作与最终Web图像的创建无关——它只是为了生成一个尺寸合适的画布供用户操作。默认情况下,控件会生成一个画布,其缩放比例为用户浏览器窗口宽度或高度的4/5,具体取决于上传图像的宽高比。这保证了图像始终可见,无需滚动,无论用户浏览器窗口的大小如何。客户端脚本在表单提交之前,将浏览器窗口(“视口”)的尺寸存储在一个隐藏的表单字段中。

Canvas

该控件在定位为浮动在控件上方的 DIV 中渲染画布(以及一些附带的 UI)。尽管此 DIV 的渲染 HTML 嵌套在控件的渲染 HTML 中,但 DIV 本身需要占用尽可能多的屏幕空间。如果它必须限制在控件默认设置所占用的空间内,我们就不会有一个可用的画布。因此,客户端脚本会使用 CSS 绝对定位和 z-index 属性,将画布 DIV 重新定位,使其脱离文档的正常流程,并允许它浮动在页面其余部分之上。

在生成的画布上方是选择矩形。这是另一个带有虚线边框的 DIV

selection rectangle

客户端脚本响应鼠标移动事件,以便当指针靠近 DIV 的边缘时,光标变为调整大小图标;当指针在 DIV 的主体上方时,光标变为移动图标。用户可以移动和调整选择框的大小以选择适当的裁剪区域。当只指定一个尺寸(ImageWidthImageHeight)时,裁剪矩形的形状不受限制——控件将缩放图像,使指定尺寸正确,而另一个尺寸将根据用户选择的裁剪结果而定。如果指定了两个尺寸,则需要强制执行特定的宽高比(宽度/高度),因此客户端脚本会在用户拖动角点时将裁剪矩形限制在此宽高比内。

proportional resizing of selection

选择满意的裁剪后,用户按下“确定”按钮。客户端脚本存储相对于画布图像的选择矩形的位置和大小,并启动回发。在服务器上,这些客户端坐标被转换为原始原始图像上的坐标(控件知道原始原始图像和它为用户“绘制”的画布的尺寸)。然后,原始原始图像被裁剪和缩放,并根据指定为控件属性的格式和质量进行保存。新创建的Web图像显示在控件中。

finished web image

该控件公开了一个属性,允许开发人员稍后访问创建的Web图像(例如,用于存储在内容管理系统中)。

另外两个功能有助于可用性。使用此控件的典型场景是为新闻报道提供两张相关图片——一张主图和一张缩略图。在编辑界面中,将呈现两个控件实例,一个用于主图,一个用于缩略图。第一个可能将 `ImageWidth` 设置为“203”,`ImageHeight` 设置为“*”,而缩略图的控件可能将 `ImageWidth` 和 `ImageHeight` 都设置为“66”。用户通常希望将相同的原始图像用于两张Web图片,但裁剪不同。为了避免用户多次上传相同的原始图像,该控件会为每个上传的原始文件制作自己的缩略图(不要与用户可能使用该控件创建的、恰好用作其他地方缩略图的任何图像混淆),并在用户会话中存储一个用于识别已上传图像的键。该控件会检查会话以查看是否已上传任何图像,如果找到,则生成一个UI以选择已上传的图像。用户只需单击图像即可直接进入画布,从而绕过可能耗时的上传过程。

thumbnails UI

另一个特性适用于用户已经拥有正确尺寸的“Web就绪”图像的情况。如果控件发现上传的图像已经正确,它将绕过画布阶段,直接将原始文件保存为最终图像文件。

点击这里查看控件实际效果.

那么这一切是如何运作的呢?

共有八个源文件

  • WebImageMaker.cs

    服务器控件类 `WebImageMaker` 的源代码。还包含三个枚举的定义——`ControlMode`,用于跟踪控件当前正在做什么(例如,显示画布),以及 `WebImageFormat` 和 `WebImageQuality`,它们也是控件的属性,允许开发人员强制最终 Web 图像的创建方式。

  • IImageProvider.cs

    一个接口,定义了控件需要对图像执行的操作。通过封装这个接口,我们将来可以切换到不同的图像库。大部分文件 I/O 也在其中完成,因为绘图 API 通常具有从磁盘读取和写入图像的能力,将 I/O 操作与绘图操作分开可能会不必要地笨拙。

  • ImageProviderImpl.cs

    一个使用System.Drawing中GDI+库的IImageProvider实现。

  • WebImageMakerImageHelper.cs

    一个帮助类,用于服务控件创建的图像。这被分离出来,以便可选地允许控件生成的图像由不同的处理程序提供服务。

  • WebImageMakerHandler.ashx

    如果您不想让控件本身处理其生成的图像的服务,则使用的可选 HttpHandler。

  • WebImageMaker.css

    客户端样式表,用于设置控件渲染的各种元素的CSS属性。

  • WebImageMaker_canvas.js

    当控件处于Canvas模式时,驱动用户界面的客户端脚本。

  • WebImageMaker_normal.js

    当控件不处于Canvas模式时,用于显示先前上传的原始图像文件的缩略图的客户端脚本。

嵌入式资源

最后三个文件是控件使用的脚本和 CSS 资源。在 ASP.NET 2.0 中,我们可以将这些资源嵌入到我们的程序集中,以便控件可以作为单个 DLL 部署到其他解决方案中,而无需依赖文件。浏览器对这些文件的请求通过 *.axd 处理程序直接指向程序集 DLL 本身。然而,Visual Web Developer 2005 Express 的用户没有构建控制库的项目选项,这使得在 IDE 中很容易实现。为此,你通常需要 Visual Studio 2005。在 Web Developer Express 中开发 ASP.NET 服务器控件是可能的,然后,在命令行编译器的帮助下,将其构建为带有嵌入式资源的控制库。在开发中,我们可以直接将这些文件作为项目的一部分使用。在构建为控件时,我们可以将它们用作嵌入式资源。

#if BuildAsControlLibrary
cssUrl = Page.ClientScript.GetWebResourceUrl(this.GetType(), 
         "Guild.WebControls.WebImageMaker.css");
#else
cssUrl = Page.ResolveUrl("~/WebImageMaker.css");
#endif

BuildAsControlLibrary”是我们项目中未定义的条件编译指令,因此在从Web Developer Express运行时,将始终使用#else子句。在WebImageMaker.cs中,我们还条件编译了三个Web资源属性,以将文件注册为程序集的一部分并定义它们应作为Mime类型提供服务

#if BuildAsControlLibrary
[assembly: WebResource("Guild.WebControls.WebImageMaker.css", 
    "text/css")]
[assembly: WebResource("Guild.WebControls.WebImageMaker_canvas.js", 
    "application/x-javascript")]
[assembly: WebResource("Guild.WebControls.WebImageMaker_normal.js", 
    "application/x-javascript")]
#endif

当从Web Developer Express正常运行项目时,文件将像其他文件一样从文件系统提供。当您想将项目编译成DLL时,可以使用提供的Build_Control_Library.bat文件,其中只包含一行

csc @Build_Switches.rsp App_Code\*.cs

其中,Build_Switches.rsp 是一个包含所需编译器开关的文件

# define our conditional compilation label
/define:BuildAsControlLibrary

# embed the resources
/resource:WebImageMaker.css,
          Guild.WebControls.WebImageMaker.css 
/resource:WebImageMaker_canvas.js, 
          Guild.WebControls.WebImageMaker_canvas.js 
/resource:WebImageMaker_normal.js,
          Guild.WebControls.WebImageMaker_normal.js 

# output as a library into our BuildOutput directory
/target:library /out:BuildOutput\
                 Guild.WebControls.WebImageMaker.dll

请注意 `BuildAsControlLibrary` 的定义以及三个资源的嵌入。`BuildOutput` 目录中的输出可以复制到其他 Web 项目中,而不会影响原始项目。

您需要一个具有正确环境变量的命令提示符。最简单的方法是,如果您有 Visual Studio 命令提示符,请使用它;如果您有 Web Developer Express,可以使用 SDK 命令提示符(可从“Microsoft .NET Framework SDK v2.0”程序组的“开始”菜单中获取)。我认为 Express 版没有安装 SDK——它值得一试。

上传文件的存储

该控件使用文件系统来存储上传的文件以及生成的画布、缩略图和网页图像。可以使用存储在用户Session中的内存流,避免ASP.NET存储图像需要可写入目录的需求,但是如果同时有几个人以上使用系统,这可能会很快失控。通过在回发之间将图像保存到文件系统,并尽快处理任何占用内存的资源,我们使系统更具可扩展性。将图像保存到磁盘还可以提供审计跟踪,以便我们可以查看用户上传了哪些类型的图像。这方面的缺点是图像目录(尤其是原始图像目录)可能会很快填满,因此控件公开了一个委托,允许开发人员以方法的形式提供清理策略,该方法将在控件写入目录之前调用。如果没有提供其他清理策略,则使用默认的清理策略。

public delegate void PurgeMethod(string directoryToClean);

生成图像的服务

除了控件最初设置的图像外,控件生成的所有图像都需要以某种方式提供服务。一种方法是使用 HTTP 处理程序,形式为 `ashx` 文件。控件有一个可选的 `HandlerPath` 属性,应该设置为指向该文件。该文件作为项目的一部分提供。但是,它破坏了“无依赖”部署场景,因为它是一个附加文件,并且还需要在 `Web.Config` 中进行一个条目来告诉它工作目录在哪里。另一种方法是让控件本身负责为其生成的图像提供服务。在没有 `HandlerPath` 属性的情况下,控件将写入生成图像的 URL,指向控件的包含页面,并带上查询字符串参数,控件可以读取这些参数,并可选择劫持页面请求以提供所需的图像。这会尽可能早地完成,以防止在服务器上进行任何不必要的工作。

protected override void OnInit(EventArgs e)
{
    if (Page.Request.QueryString["mode" + KeySuffix] != null)
    {
        ...
        // get the details of the image from the querystring
        ...
        // write the image out to the response
        ...
        Response.End();
    }
    ...
}

实际上,读取查询字符串并写入文件的代码被分离到 `WebImageMakerImageHelper` 类中,并且由控件和提供的处理程序(`WebImageMakerHandler.ashx`)共同使用。

该控件有一个私有属性(IsServingImage),它维护该属性以确保在调用 OnInit 之前不会执行不必要的工作。这可以在代码的多个地方看到。

关于控件以这种方式劫持其包含页面请求的优缺点,请参阅 Fritz Onion的博客上的帖子讨论。

虽然让控件自己生成图片很巧妙,但正如上面帖子中的讨论所见,它也存在一些问题,尤其是 ASP.NET 在处理页面请求以及在调用您的特定控件的 `OnInit` 方法(您可以在那里提前终止请求)之前可能做的其他工作的潜在未知量。在某些情况下,这项工作可能绝非无关紧要。总的来说,我会尽可能使用独立的处理程序。项目提供的示例页面在不同的控件实例中使用了两种方法。

子控件

该控件派生自新的 ASP.NET 2.0 CompositeControl 抽象类。这处理了 v1.1 中必须手动完成的一些事情,也允许控件在设计器中渲染,而无需太多额外的工作。

该控件的子控件都是简单的Web控件和HTML控件,带有一些文字。`WebImageMaker` 控件的一些属性映射到子控件的属性

[Bindable(false)]
[Category("Appearance")]
[DefaultValue("Confirm Selection")]
[Themeable(false)]
public string ConfirmButtonText
{
    get
    {
        EnsureChildControls();
        return confirmSelection.Text;
    }
    set
    {
        if (!IsServingImage) // child controls won't be instantiated
        {
             EnsureChildControls();
             confirmSelection.Text = value;
        }
    }
}

此处,`confirmSelection` 是构成画布 UI 的一个按钮。调用 `EnsureChildControls()` 会导致 ASP.NET 调用控件的 `CreateChildControls()` 方法,该方法会创建控件层次结构。

CreateChildControls()

请注意,大部分代码已被剥离——请参阅提供的源代码以获取完整信息。

protected override void CreateChildControls()
{
    ...
    targetImage = new HtmlImage();
    popupDiv = new HtmlGenericControl("div");
    upload = new FileUpload(); 
    // etc... and the rest of the controls
    ...

    targetImage.ID = this.ID + "_img"; 
    popupDiv.Attributes.Add("class", "webImageMaker_popup");
    // etc... add attributes to controls
    // that need them for css and script
    ...

    this.Controls.Add(popupDiv);
    this.Controls.Add(hiddenField);            
    popupDiv.Controls.Add(canvas);
    popupDiv.Controls.Add(selectionBox);
    // etc... add the controls to the hierarchy
    ...

    // some controls need to fire both
    // client- and server-side events:
    confirmSelection.OnClientClick = 
        "storeSelectionInfo('" + 
        hiddenField.ClientID + "')";
    confirmSelection.Click += 
      new EventHandler(confirmSelection_Click);
    ...

    foreach (string thumbnailFilename in SessionImages)
    {
        ImageButton thumbBtn = 
               getThumbnailButton(thumbnailFilename);
        thumbBtn.Command += new 
               CommandEventHandler(btnThumb_Command);
        thumbnailsDiv.Controls.Add(thumbBtn);
    }
    ...

    thumbnailButton.Attributes.Add("onclick", 
        "showThumbnailDiv('" + thumbnailsDiv.ClientID + "');");
    uploadButton.OnClientClick 
        = "setViewportDimensions('" + hiddenField.ClientID + "')";
    this.Controls.Add(upload);
    this.Controls.Add(uploadButton);
    uploadButton.Click += new 
          EventHandler(uploadButton_Click);
    ...

    this.ChildControlsCreated = true;
}

所有可能被控件使用的子控件都在此方法中实例化并构建到控件层次结构中,尽管其中一些控件最终不会在以后渲染。一些控件被赋予特定的ID和样式属性,以便渲染的元素可以与客户端JavaScript和CSS配合使用。所有控件都需要存在以响应稍后可能触发的任何事件。`CreateChildControls()` 通常在控件生命周期的早期被调用,在处理任何事件之前,并且在我们确切知道控件应该处于什么状态之前。等到我们稍后重写`Render`方法时,我们确切知道控件应该是什么样子,我们可以有选择地选择我们实际想要渲染到客户端的控件树的哪些部分。

CreateChildControls() 构建的完整控制层次结构如下所示

Control layout

客户端JavaScript和CSS负责将 `popupDIV` 显示为一个“浮动”窗口,其中选择框 DIV(显示为虚线轮廓矩形)浮动在画布图像上方。一旦用户已经上传至少一张图像,`thumbnailsDiv` 元素也将被渲染——这允许用户选择以前上传的原始图像。

该控件通过其 `controlMode` 属性跟踪其当前状态。这是一个枚举,它在高级别定义了控件可能处于的三种状态。

public enum ControlMode
{
    Normal, Canvas, Changed
}

Normal 是初始状态。Canvas 用于向用户显示绘图表面且UI正在等待用户输入时。如果用户点击“确定”并创建了Web图像,状态将变为Changed。从Changed状态,控件可以在CanvasChanged之间来回切换,但不能返回到Normal。如果用户在Canvas状态下取消,状态可以从Normal变为Canvas并再次返回。

在较低层面,通过使用 ASP.NET 2.0 新的 ControlState 功能来跟踪控件的状态。这与 ViewState 非常相似,只是在 ViewState 被禁用时仍然可以访问。ControlState 作为单个对象进出控件。

protected override void LoadControlState(object savedState)...
protected override object SaveControlState()...

在此控件中,`ControlState` 以字符串数组的形式持久化。请注意,我们必须在 `OnInit` 方法中告知包含页面我们希望使用其 `ControlState` 服务。

Page.RegisterRequiresControlState(this);

这确保了 `Page` 将调用 `Load` 和 `Save` 方法对。

渲染

protected override void Render(HtmlTextWriter writer)
{
   AddAttributesToRender(writer);
   writer.RenderBeginTag(HtmlTextWriterTag.Div);
           
  if (this.controlMode == ControlMode.Canvas)
  {
      popupDiv.RenderControl(writer)
  }
  ...
  ...
}

在渲染阶段,控件有选择地要求其每个子控件将自身渲染到所提供的 `HtmlTextWriter`。`AddAttributesToRender` 确保在控件上设置的任何属性,如果它们是 `WebControl` 类属性而不是我们派生的 `WebImageMaker` 类属性,则作为属性写入 HTML 元素,我们在下一行声明该元素为 DIV 元素。

构成画布UI的popupDiv仅在控件(响应文件上传并成功读取为图像后)确定其处于Canvas模式时才渲染。类似地,缩略图UI仅在用户之前上传过图像时才渲染。

if (SessionImages.Count > 0)
{
    thumbnailButton.RenderControl(writer);
    writer.WriteBreak();
    thumbnailsDiv.RenderControl(writer)
}

SessionImages是一个属性,它返回控件以前生成的缩略图名称列表。此列表存储在用户的会话中。

上传图片

在 `CreateChildControls()` 中,我们设置了 `uploadButton` 的 `OnClientClick` 属性,以便在表单提交之前调用一些客户端脚本。

uploadButton.OnClientClick 
    = "setViewportDimensions('" + hiddenField.ClientID + "')";

这会调用 WebImageMaker_normal.js 中的以下函数

function setViewportDimensions(hiddenFieldID)
{
    var field = document.getElementById(hiddenFieldID);
    var width;
    var height;
    
    if (window.innerWidth)
    {
        width = window.innerWidth;
        height = window.innerHeight;
    }
    else if (document.documentElement && 
             document.documentElement.clientWidth)
    {
        width = document.documentElement.clientWidth;
        height = document.documentElement.clientHeight;
    }
    else if (document.body)
    {
        width = document.body.clientWidth;
        height = document.body.clientHeight;
    }
    field.value = width + "," + height;
}

其作用是将浏览器的视口尺寸编码在一个隐藏的表单字段中。视口是浏览器窗口的内部尺寸。“if...”语句中的三种不同条件适应了各种浏览器差异。在 www.quirksmode.org 上有关于视口的精彩讨论,这是一个关于 CSS、JavaScript 和浏览器特性的绝佳资源。

回到服务器端,`uploadButton` 的点击事件由以下处理程序处理:

void uploadButton_Click(object sender, EventArgs e)
{
    // do we have a file?
    if (!upload.HasFile)
    {
        lblMessages.Text = "No file present. Might be too big.";
        lblMessages.Visible = true;
        return;
    }

    // is it an image?
    string thumbnailFileName;
    bool imageOK = 
         ImageProvider.SaveRaw(upload.PostedFile, 
         out serverImgID, out rawWidth, out rawHeight, 
         out thumbnailFileName);
    if (!imageOK)
    {
        lblMessages.Text = "File is not an image" + 
                           " that the system understands.";
        lblMessages.Visible = true;
        return;
    }

    // we've got this far, so make
    // a thumbnail and store the Guid in the user's 
    // session, so they can reuse
    // the image without having to upload it again.
    SessionImages.Add(thumbnailFileName);

    CanvasFromRaw();            
}

只要 `FileUpload` 控件实际包含文件,控件就会将上传的图像交给我们的 `IImageProvider` 实现来保存到文件系统,并在过程中生成缩略图。控件的 `ImageProvider` 属性是 `IImageProvider` 实现的访问器。

private IImageProvider __imageProvider;
private IImageProvider ImageProvider
{
    get
    {
        if (__imageProvider == null)
        {
            __imageProvider = new ImageProviderImpl();
            __imageProvider.WorkingDirectory = this.workingDirectory;
            __imageProvider.ServerID = this.serverImgID;
            __imageProvider.ThumbnailSize = this.thumbnailSize;
            __imageProvider.PurgeStrategy = this.purgeStrategy;
        }
        return __imageProvider;
    }
}

由于只有一种 `IImageProvider` 的实现,我们只需实例化 `ImageProviderImpl` 对象,并向其传递所需的各种属性。如果需要,每个请求针对每个控件创建一次此对象——它不会在请求之间持久化。当 `WorkingDirectory` 属性在我们的 `ImageProviderImpl` 实例上设置时,它会确保四个子目录也存在,分别用于原始图像、画布图像、缩略图和 Web 图像。`ServerID` 属性是一个字符串,唯一标识控件当前正在处理的图像。它由 `IImageProvider` 在保存新原始图像时生成,但控件也需要知道它——此属性用于在后续回发中命名生成的画布、缩略图和 Web 图像文件,因此控件将其持久化在 `ControlState` 中。

ImageProviderImplSaveRaw 实现如下:

public bool SaveRaw(HttpPostedFile postedFile, 
       out string outServerID, out int rawWidth, 
       out int rawHeight, out string thumbnailFileName)
{
    purge(WebImageMaker.RawImageDirName);
    this.serverID = outServerID = Guid.NewGuid().ToString();
    string filepath = getRawFilePath();
    postedFile.SaveAs(filepath);
    return getRawInfo(
       filepath, out rawWidth, out rawHeight, 
       true, out thumbnailFileName);
}

控件传入上传的文件,`SaveRaw` 返回一个新的服务器ID、图像尺寸(`rawWidth` 和 `rawHeigth`)以及它为该图像创建的缩略图文件的名称。在此实现中,生成的服务器ID是一个GUID,这似乎是一个明智的选择。如果读取上传文件作为图像时出现任何问题,`SaveRaw` 将返回 false。这项工作由 `getRawInfo` 处理。

private bool getRawInfo(string filepath, out int rawWidth, 
        out int rawHeight, bool createThumbnail, 
        out string thumbnailFileName)
{
    thumbnailFileName = "";
    bool result = false;
    rawWidth = 0;
    rawHeight = 0;
    try
    {
        using (Image img = Image.FromFile(filepath))
        {
            rawWidth = img.Width;
            rawHeight = img.Height;
            rawFormat = img.RawFormat;
            result = true;
            if (createThumbnail)
            {
                thumbnailFileName = CreateThumbnail(img, 
                                    WebImageFormat.Jpg);
            }
        }
    }
    catch
    {
        result = false;
    }
    return result;
}

getRawInfo 并不总是生成缩略图——当用户点击缩略图而不是上传原始图像时,它也会被调用。控件中的同一个事件处理程序处理任何缩略图的点击事件。

void btnThumb_Command(object sender, CommandEventArgs e)
{            
    bool imageOK = 
         ImageProvider.UseThumbnailFile(
         e.CommandArgument.ToString(), 
         out serverImgID, out rawWidth, 
         out rawHeight);
    if (!imageOK)
    {
        lblMessages.Text = 
            "Could not find the uploaded image" + 
            " corresponding to the thumbnail.";
        lblMessages.Visible = true;
        return;
    }
    CanvasFromRaw();
}

缩略图都是通过在 `CreateChildControls` 阶段调用 `getThumbnailButton` 创建的 `ImageButton`。它们被赋予了一个命令参数,该参数是缩略图文件的名称,`ImageProvider` 稍后可以从中获取服务器 ID。`btnThumb_Command` 处理程序执行与我们之前看到的 `uploadButton_Click` 相同的工作,但它不调用 `ImageProvider` 的 `SaveRaw` 方法,而是调用 `UseThumbnailFile`。

public bool UseThumbnailFile(string thumbnailFileName, 
       out String outServerID, out int rawWidth, 
       out int rawHeight)
{
    this.serverID = thumbnailFileName.Substring(
        0, thumbnailFileName.LastIndexOf("."));
    outServerID = serverID;
    string dummy; 
    return getRawInfo(
        getRawFilePath(), out rawWidth, 
        out rawHeight, false, out dummy);
}

IImageProvider 实现始终可以信任从文件名获取服务器 ID,因为它最初总是从服务器 ID 生成该文件名。

因此,无论用户上传新文件,还是选择缩略图重用以前上传的文件,`ImageProviderImpl` 中的代码都会到达 `getRawInfo` 以将图像的详细信息返回给控件,并在图像加载时选择性地创建缩略图。生成缩略图让我们首次了解代码如何在不同阶段生成所需的各种图像。

private string CreateThumbnail(Image img, WebImageFormat format)
{
    string thumbFileName = null;
    Rectangle rawRect = new Rectangle(0, 0, img.Width, img.Height);
    Rectangle thumbRect = new Rectangle();
    float fWidth = (float)img.Width;
    float fHeight = (float)img.Height;
    float fThumbSize = (float)thumbnailSize;
    float aspectRatio = fWidth / fHeight;
    if (aspectRatio > 1)
    {
        thumbRect.Width = thumbnailSize;
        thumbRect.X = 0;
        thumbRect.Height = Convert.ToInt32((fThumbSize / 
                                     fWidth) * fHeight);
        thumbRect.Y = (thumbnailSize - thumbRect.Height) / 2;
    }
    else
    {
        thumbRect.Height = thumbnailSize;
        thumbRect.Y = 0;
        thumbRect.Width = Convert.ToInt32((fThumbSize / 
                                    fHeight) * fWidth);
        thumbRect.X = (thumbnailSize - thumbRect.Width) / 2;
    }
    using (Bitmap thumb = new Bitmap(
        thumbnailSize, thumbnailSize, 
        PixelFormat.Format24bppRgb))
    {
        using (Graphics g = Graphics.FromImage(thumb))
        {
            setGraphicsQuality(g, WebImageQuality.High);
            g.Clear(Color.White);
            g.DrawImage(img, thumbRect, rawRect, 
                        GraphicsUnit.Pixel);
            string filepath = getFilePath(
                WebImageMaker.ThumbnailImageDirName, format);
            thumb.Save(filepath, getGDIFormat(format));
            thumbFileName = Path.GetFileName(filepath);
        }
    }
    return thumbFileName;
}

控件为其自身UI生成的缩略图总是正方形的(它只提供一个 `ThumbnailSize` 属性),但上传的图像只会巧合地是正方形。我们需要将上传的图像缩小,使其最长边与指定的 `ThumbnailSize` 匹配,然后将这个缩放后的矩形图像在正方形缩略图中居中。因此,除了计算我们需要将原始图像缩放到的宽度和高度外,我们还需要计算缩放后的矩形图像在正方形缩略图的左侧或顶部需要定位多远。我们使用一个名为 `thumbRect` 的 `System.Drawing.Rectangle` 来保存此信息,因为它同时维护大小(宽度、高度)和位置(X、Y)。

一旦我们知道缩放后的图像将位于缩略图的何处,我们就为缩略图创建一个新的正方形位图。

using (Bitmap thumb = new Bitmap(
    thumbnailSize, thumbnailSize, 
    PixelFormat.Format24bppRgb));

并从该位图中获取一个绘图表面 `g`(一个 `System.Drawing.Graphics` 实例)。

using (Graphics g = Graphics.FromImage(thumb));

接下来调用的 `setGraphicsQuality` 方法被 `ImageProviderImpl` 多次使用。为了向开发人员提供简单的 API,我们定义了一个独立于任何图形 API 且对用户显而易见的枚举。

public enum WebImageQuality
{
    High, Medium, Low
}

然后,开发人员可以轻松地将最终Web图像的所需图像质量设置为控件的属性。

... Quality="High" ...

在使用GDI+等特定库时,我们需要将这种通用的质量概念转换为特定于库的设置,因此:

private void setGraphicsQuality(Graphics g, WebImageQuality quality)
{
    switch (quality)
    {
        case WebImageQuality.High:
        g.InterpolationMode = 
            System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
        g.PixelOffsetMode = 
            System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
        g.SmoothingMode = 
            System.Drawing.Drawing2D.SmoothingMode.HighQuality;
        break;

        case WebImageQuality.Medium:
        g.InterpolationMode = 
            System.Drawing.Drawing2D.InterpolationMode.Default;
        g.PixelOffsetMode = 
            System.Drawing.Drawing2D.PixelOffsetMode.Half;
        g.SmoothingMode = 
            System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
        break;

        case WebImageQuality.Low:
        g.InterpolationMode = 
            System.Drawing.Drawing2D.InterpolationMode.Low;
        g.PixelOffsetMode = 
            System.Drawing.Drawing2D.PixelOffsetMode.HighSpeed;
        g.SmoothingMode = 
            System.Drawing.Drawing2D.SmoothingMode.HighSpeed;
        break;
    }
}

在这种情况下,高质量对应于GDI+提供的最高可能质量设置。

在缩略图生成的情况下,我们实际上忽略了用户的设置,并始终追求最高的设置,因为否则将大图像缩小到如此小的尺寸将导致质量非常差的缩略图。但在其他地方(在画布生成和最终Web图像生成中),我们使用控件属性。

回到缩略图生成代码中,我们将绘图表面涂成白色,然后使用 `drawImage` 的重载将缩小后的图像绘制到指定位置。

g.Clear(Color.White);
g.DrawImage(img, thumbRect, rawRect, GraphicsUnit.Pixel);

thumbRect 是目标矩形,rawRect 是相对于 img(原始图像)的源矩形。在这种情况下,rawRect 是整个原始图像,但稍后,我们将使用类似的技术来获取裁剪区域。

然后我们调用 `getFilePath` 来确定我们要将缩略图保存到哪里以及要给它起什么名字。这个方法利用了控件中定义的常量。

public const string RawImageDirName = "raw";
public const string CanvasImageDirName = "canvas";
public const string ThumbnailImageDirName = "thumbnails";
public const string WebImageDirName = "web";

这些名称指定了主工作目录下存储每种类型图像的子目录。`getFilePath` 方法使用服务器 ID 和格式(GIF、PNG 或 JPG)来命名文件。如果用户正在从以前上传的图像制作新图像,则原始原始图像的服务器 ID 将已用于命名画布和 web 目录中的至少一个文件,因此我们还会检查现有文件并在必要时重命名文件,使用 Windows 的版本号约定(例如,myfile(3).jpg)。

然后我们以指定的格式保存缩略图。`getGDIFormat(..)` 类似于 `getGraphicsQuality(..)`,它将我们简单的 `WebImageFormat` 枚举转换为 GDI+ 特定的内容,在本例中,是到 GDI+ `ImageFormat` 实例的简单映射。

历史

  • 2006年2月22日:首次发布
  • 2009年2月17日:更新了演示项目
© . All rights reserved.