diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/AdminMenu.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/AdminMenu.cs new file mode 100644 index 000000000..f5b221389 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/AdminMenu.cs @@ -0,0 +1,17 @@ +using Orchard.Localization; +using Orchard.Security; +using Orchard.UI.Navigation; + +namespace Orchard.MediaProcessing { + public class AdminMenu : INavigationProvider { + public Localizer T { get; set; } + + public string MenuName { + get { return "admin"; } + } + + public void GetNavigation(NavigationBuilder builder) { + builder.Add(T("Media"), "6", item => item.Add(T("Profiles"), "5", i => i.Action("Index", "Admin", new {area = "Orchard.MediaProcessing"}).Permission(StandardPermissions.SiteOwner))); + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Controllers/AdminController.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Controllers/AdminController.cs new file mode 100644 index 000000000..f06a4cb2d --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Controllers/AdminController.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web.Mvc; +using System.Web.Routing; +using Orchard.ContentManagement; +using Orchard.Core.Contents.Controllers; +using Orchard.DisplayManagement; +using Orchard.Forms.Services; +using Orchard.Localization; +using Orchard.MediaProcessing.Descriptors.Filter; +using Orchard.MediaProcessing.Models; +using Orchard.MediaProcessing.Services; +using Orchard.MediaProcessing.ViewModels; +using Orchard.Security; +using Orchard.Settings; +using Orchard.UI.Navigation; +using Orchard.UI.Notify; + +namespace Orchard.MediaProcessing.Controllers { + [ValidateInput(false)] + public class AdminController : Controller, IUpdateModel { + private readonly ISiteService _siteService; + private readonly IImageProfileService _profileService; + private readonly IImageProcessingManager _imageProcessingManager; + + public AdminController( + IOrchardServices services, + IShapeFactory shapeFactory, + ISiteService siteService, + IImageProfileService profileService, + IImageProcessingManager imageProcessingManager) { + _siteService = siteService; + _profileService = profileService; + _imageProcessingManager = imageProcessingManager; + Services = services; + + T = NullLocalizer.Instance; + Shape = shapeFactory; + } + + private dynamic Shape { get; set; } + public IOrchardServices Services { get; set; } + public Localizer T { get; set; } + + public ActionResult Index(AdminIndexOptions options, PagerParameters pagerParameters) { + if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to list media profiles"))) + return new HttpUnauthorizedResult(); + + var pager = new Pager(_siteService.GetSiteSettings(), pagerParameters); + + // default options + if (options == null) + options = new AdminIndexOptions(); + + var profiles = Services.ContentManager.Query("ImageProfile"); + + var pagerShape = Shape.Pager(pager).TotalItemCount(profiles.Count()); + + profiles = profiles.Join().OrderBy(u => u.Name); + + var results = profiles + .Slice(pager.GetStartIndex(), pager.PageSize) + .ToList(); + + var model = new AdminIndexViewModel { + ImageProfiles = results.Select(x => new ImageProfileEntry { + ImageProfile = x.As().Record, + ImageProfileId = x.Id, + Name = x.As().Name + }).ToList(), + Options = options, + Pager = pagerShape + }; + + // maintain previous route data when generating page links + var routeData = new RouteData(); + routeData.Values.Add("Options.Filter", options.Filter); + + pagerShape.RouteData(routeData); + + return View(model); + } + + [HttpPost] + [FormValueRequired("submit.BulkEdit")] + public ActionResult Index(FormCollection input) { + if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage media profiles"))) + return new HttpUnauthorizedResult(); + + var viewModel = new AdminIndexViewModel {ImageProfiles = new List(), Options = new AdminIndexOptions()}; + UpdateModel(viewModel); + + var checkedItems = viewModel.ImageProfiles.Where(c => c.IsChecked); + + switch (viewModel.Options.BulkAction) { + case ImageProfilesBulkAction.None: + break; + case ImageProfilesBulkAction.Delete: + foreach (var checkedItem in checkedItems) { + _profileService.DeleteImageProfile(checkedItem.ImageProfileId); + } + + break; + default: + throw new ArgumentOutOfRangeException(); + } + + return RedirectToAction("Index"); + } + + public ActionResult Edit(int id) { + if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to edit media profiles"))) + return new HttpUnauthorizedResult(); + + var profile = _profileService.GetImageProfile(id); + var viewModel = new AdminEditViewModel { + Id = profile.Id, + Name = profile.Name + }; + + var filterEntries = new List(); + var allFilters = _imageProcessingManager.DescribeFilters().SelectMany(x => x.Descriptors).ToList(); + + foreach (var filter in profile.Filters.OrderBy(f => f.Position)) { + var category = filter.Category; + var type = filter.Type; + + var f = allFilters.FirstOrDefault(x => category == x.Category && type == x.Type); + if (f != null) { + filterEntries.Add( + new FilterEntry { + Category = f.Category, + Type = f.Type, + FilterRecordId = filter.Id, + DisplayText = String.IsNullOrWhiteSpace(filter.Description) ? f.Display(new FilterContext {State = FormParametersHelper.ToDynamic(filter.State)}).Text : filter.Description + }); + } + } + + viewModel.Filters = filterEntries; + + return View(viewModel); + } + + [HttpPost] + public ActionResult Delete(int id) { + if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage media profiles"))) + return new HttpUnauthorizedResult(); + + var profile = _profileService.GetImageProfile(id); + + if (profile == null) { + return HttpNotFound(); + } + + Services.ContentManager.Remove(profile.ContentItem); + Services.Notifier.Information(T("Image Profile {0} deleted", profile.Name)); + + return RedirectToAction("Index"); + } + + public ActionResult Move(string direction, int id, int filterId) { + if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage media profiles"))) + return new HttpUnauthorizedResult(); + + switch (direction) { + case "up": + _profileService.MoveUp(filterId); + break; + case "down": + _profileService.MoveDown(filterId); + break; + default: + throw new ArgumentException("direction"); + } + + return RedirectToAction("Edit", new {id}); + } + + public ActionResult Preview(int id) { + if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage media profiles"))) + return new HttpUnauthorizedResult(); + + throw new NotImplementedException(); + } + + bool IUpdateModel.TryUpdateModel(TModel model, string prefix, string[] includeProperties, string[] excludeProperties) { + return TryUpdateModel(model, prefix, includeProperties, excludeProperties); + } + + public void AddModelError(string key, LocalizedString errorMessage) { + ModelState.AddModelError(key, errorMessage.ToString()); + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Controllers/FilterController.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Controllers/FilterController.cs new file mode 100644 index 000000000..fd85bf4e6 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Controllers/FilterController.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Web.Mvc; +using Orchard.Caching; +using Orchard.Data; +using Orchard.DisplayManagement; +using Orchard.FileSystems.Media; +using Orchard.Forms.Services; +using Orchard.Localization; +using Orchard.MediaProcessing.Models; +using Orchard.MediaProcessing.Services; +using Orchard.MediaProcessing.ViewModels; +using Orchard.Security; +using Orchard.UI.Admin; +using Orchard.UI.Notify; +using FormParametersHelper = Orchard.Forms.Services.FormParametersHelper; + +namespace Orchard.MediaProcessing.Controllers { + [ValidateInput(false), Admin] + public class FilterController : Controller { + public FilterController( + IOrchardServices services, + IFormManager formManager, + IShapeFactory shapeFactory, + IImageProcessingManager processingManager, + IRepository repository, + IImageProfileService profileService, + ISignals signals) { + Services = services; + _formManager = formManager; + _processingManager = processingManager; + _filterRepository = repository; + _profileService = profileService; + _signals = signals; + Shape = shapeFactory; + } + + public IOrchardServices Services { get; set; } + private readonly IFormManager _formManager; + private readonly IImageProcessingManager _processingManager; + private readonly IRepository _filterRepository; + private readonly IImageProfileService _profileService; + private readonly ISignals _signals; + private readonly IStorageProvider _storageProvider; + public Localizer T { get; set; } + public dynamic Shape { get; set; } + + public ActionResult Add(int id) { + if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage image profiles"))) + return new HttpUnauthorizedResult(); + + var viewModel = new FilterAddViewModel {Id = id, Filters = _processingManager.DescribeFilters()}; + return View(viewModel); + } + + public ActionResult Delete(int id, int filterId) { + if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage image profiles"))) + return new HttpUnauthorizedResult(); + + var filter = _filterRepository.Get(filterId); + if (filter == null) { + return HttpNotFound(); + } + + _filterRepository.Delete(filter); + Services.Notifier.Information(T("Filter deleted")); + + return RedirectToAction("Edit", "Admin", new {id}); + } + + public ActionResult Edit(int id, string category, string type, int filterId = -1) { + if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not authorized to manage image profiles"))) + return new HttpUnauthorizedResult(); + + var filter = _processingManager.DescribeFilters().SelectMany(x => x.Descriptors).FirstOrDefault(x => x.Category == category && x.Type == type); + + if (filter == null) { + return HttpNotFound(); + } + + // build the form, and let external components alter it + var form = filter.Form == null ? null : _formManager.Build(filter.Form); + + string description = ""; + + // bind form with existing values). + if (filterId != -1) { + var profile = _profileService.GetImageProfile(id); + var filterRecord = profile.Filters.FirstOrDefault(f => f.Id == filterId); + if (filterRecord != null) { + description = filterRecord.Description; + var parameters = FormParametersHelper.FromString(filterRecord.State); + _formManager.Bind(form, new DictionaryValueProvider(parameters, CultureInfo.InvariantCulture)); + } + } + + var viewModel = new FilterEditViewModel {Id = id, Description = description, Filter = filter, Form = form}; + return View(viewModel); + } + + [HttpPost, ActionName("Edit")] + public ActionResult EditPost(int id, string category, string type, [DefaultValue(-1)] int filterId, FormCollection formCollection) { + var profile = _profileService.GetImageProfile(id); + + var filter = _processingManager.DescribeFilters().SelectMany(x => x.Descriptors).FirstOrDefault(x => x.Category == category && x.Type == type); + + var model = new FilterEditViewModel(); + TryUpdateModel(model); + + // validating form values + _formManager.Validate(new ValidatingContext {FormName = filter.Form, ModelState = ModelState, ValueProvider = ValueProvider}); + + if (ModelState.IsValid) { + var filterRecord = profile.Filters.FirstOrDefault(f => f.Id == filterId); + + // add new filter record if it's a newly created filter + if (filterRecord == null) { + filterRecord = new FilterRecord { + Category = category, + Type = type, + Position = profile.Filters.Count + }; + profile.Filters.Add(filterRecord); + } + + var dictionary = formCollection.AllKeys.ToDictionary(key => key, formCollection.Get); + + // save form parameters + filterRecord.State = FormParametersHelper.ToString(dictionary); + filterRecord.Description = model.Description; + + // set profile as updated + profile.ModifiedUtc = DateTime.UtcNow; + profile.FileNames.Clear(); + _signals.Trigger("MediaProcessing_" + profile.Name + "_Saved"); + + return RedirectToAction("Edit", "Admin", new {id}); + } + + // model is invalid, display it again + var form = _formManager.Build(filter.Form); + + _formManager.Bind(form, formCollection); + var viewModel = new FilterEditViewModel {Id = id, Description = model.Description, Filter = filter, Form = form}; + + return View(viewModel); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Descriptors/Filter/DescribeFilterContext.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Descriptors/Filter/DescribeFilterContext.cs new file mode 100644 index 000000000..73a22a51e --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Descriptors/Filter/DescribeFilterContext.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using Orchard.Localization; + +namespace Orchard.MediaProcessing.Descriptors.Filter { + public class DescribeFilterContext { + private readonly Dictionary _describes = new Dictionary(); + + public IEnumerable> Describe() { + return _describes.Select(kp => new TypeDescriptor { + Category = kp.Key, + Name = kp.Value.Name, + Description = kp.Value.Description, + Descriptors = kp.Value.Types + }); + } + + public DescribeFilterFor For(string category) { + return For(category, null, null); + } + + public DescribeFilterFor For(string category, LocalizedString name, LocalizedString description) { + DescribeFilterFor describeFor; + if (!_describes.TryGetValue(category, out describeFor)) { + describeFor = new DescribeFilterFor(category, name, description); + _describes[category] = describeFor; + } + return describeFor; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Descriptors/Filter/DescribeFilterFor.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Descriptors/Filter/DescribeFilterFor.cs new file mode 100644 index 000000000..842519522 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Descriptors/Filter/DescribeFilterFor.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using Orchard.Localization; + +namespace Orchard.MediaProcessing.Descriptors.Filter { + public class DescribeFilterFor { + private readonly string _category; + + public DescribeFilterFor(string category, LocalizedString name, LocalizedString description) { + Types = new List(); + _category = category; + Name = name; + Description = description; + } + + public LocalizedString Name { get; private set; } + public LocalizedString Description { get; private set; } + public List Types { get; private set; } + + public DescribeFilterFor Element(string type, LocalizedString name, LocalizedString description, Action filter, Func display, string form = null) { + Types.Add(new FilterDescriptor { Type = type, Name = name, Description = description, Category = _category, Filter = filter, Display = display, Form = form }); + return this; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Descriptors/Filter/FilterContext.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Descriptors/Filter/FilterContext.cs new file mode 100644 index 000000000..6b865f4a2 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Descriptors/Filter/FilterContext.cs @@ -0,0 +1,12 @@ +using System.Drawing; +using System.Drawing.Imaging; + +namespace Orchard.MediaProcessing.Descriptors.Filter { + public class FilterContext { + public dynamic State { get; set; } + public Image Image { get; set; } + public ImageFormat ImageFormat { get; set; } + public string FilePath { get; set; } + public bool Saved { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Descriptors/Filter/FilterDescriptor.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Descriptors/Filter/FilterDescriptor.cs new file mode 100644 index 000000000..a676d77a1 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Descriptors/Filter/FilterDescriptor.cs @@ -0,0 +1,14 @@ +using System; +using Orchard.Localization; + +namespace Orchard.MediaProcessing.Descriptors.Filter { + public class FilterDescriptor { + public string Category { get; set; } + public string Type { get; set; } + public LocalizedString Name { get; set; } + public LocalizedString Description { get; set; } + public Action Filter { get; set; } + public string Form { get; set; } + public Func Display { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Descriptors/TypeDescriptor.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Descriptors/TypeDescriptor.cs new file mode 100644 index 000000000..9ea9124e4 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Descriptors/TypeDescriptor.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Orchard.Localization; + +namespace Orchard.MediaProcessing.Descriptors { + public class TypeDescriptor { + public string Category { get; set; } + public LocalizedString Name { get; set; } + public LocalizedString Description { get; set; } + public IEnumerable Descriptors { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Drivers/ImageProfilePartDriver.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Drivers/ImageProfilePartDriver.cs new file mode 100644 index 000000000..7f1019b51 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Drivers/ImageProfilePartDriver.cs @@ -0,0 +1,86 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using Orchard.ContentManagement; +using Orchard.ContentManagement.Drivers; +using Orchard.ContentManagement.Handlers; +using Orchard.Localization; +using Orchard.MediaProcessing.Models; +using Orchard.MediaProcessing.Services; +using Orchard.Utility.Extensions; + +namespace Orchard.MediaProcessing.Drivers { + + public class ImageProfilePartDriver : ContentPartDriver { + private readonly IImageProfileService _imageProfileService; + + private const string TemplateName = "Parts.MediaProcessing.ImageProfilePart"; + + public ImageProfilePartDriver(IImageProfileService imageProfileService) { + _imageProfileService = imageProfileService; + } + + public Localizer T { get; set; } + + protected override string Prefix { + get { return "MediaProcessing"; } + } + + protected override DriverResult Display(ImageProfilePart part, string displayType, dynamic shapeHelper) { + return ContentShape("Parts_MediaProcessing_ImageProfile", + () => shapeHelper.Parts_MediaProcessing_ImageProfile(Name: part.Name)); + } + + protected override DriverResult Editor(ImageProfilePart part, dynamic shapeHelper) { + return ContentShape("Parts_MediaProcessing_ImageProfile_Edit", + () => shapeHelper.EditorTemplate(TemplateName: TemplateName, Model: part, Prefix: Prefix)); + } + + protected override DriverResult Editor(ImageProfilePart part, IUpdateModel updater, dynamic shapeHelper) { + var currentName = part.Name; + updater.TryUpdateModel(part, Prefix, null, null); + if (String.IsNullOrWhiteSpace(part.Name)) { + updater.AddModelError("Name", T("The Name can't be empty.")); + } + if (currentName != part.Name && _imageProfileService.GetImageProfileByName(part.Name) != null) { + updater.AddModelError("Name", T("A profile with the same Name already exists.")); + } + if (part.Name != part.Name.ToSafeName()) { + updater.AddModelError("Name", T("The Name can only contain letters and numbers without spaces")); + } + + return Editor(part, shapeHelper); + } + + protected override void Exporting(ImageProfilePart part, ExportContentContext context) { + var element = context.Element(part.PartDefinition.Name); + element.Add( + new XElement("Filters", + part.Filters.Select(filter => + new XElement("Filter", + new XAttribute("Description", filter.Description ?? ""), + new XAttribute("Category", filter.Category ?? ""), + new XAttribute("Type", filter.Type ?? ""), + new XAttribute("Position", filter.Position), + new XAttribute("State", filter.State ?? "") + ) + ) + ) + ); + } + + protected override void Importing(ImageProfilePart part, ImportContentContext context) { + var element = context.Data.Element(part.PartDefinition.Name); + + part.Record.Filters = element.Element("Filters").Elements("Filter").Select(filter => + + new FilterRecord { + Description = filter.Attribute("Description").Value, + Category = filter.Attribute("Category").Value, + Type = filter.Attribute("Type").Value, + Position = Convert.ToInt32(filter.Attribute("Position").Value), + State = filter.Attribute("State").Value + }).ToList(); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Handlers/ImageProfilePartHandler.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Handlers/ImageProfilePartHandler.cs new file mode 100644 index 000000000..c316aa066 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Handlers/ImageProfilePartHandler.cs @@ -0,0 +1,11 @@ +using Orchard.ContentManagement.Handlers; +using Orchard.Data; +using Orchard.MediaProcessing.Models; + +namespace Orchard.MediaProcessing.Handlers { + public class ImageProfilePartHandler : ContentHandler { + public ImageProfilePartHandler(IRepository repository) { + Filters.Add(StorageFilter.For(repository)); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Media/StorageProviderExtensions.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Media/StorageProviderExtensions.cs new file mode 100644 index 000000000..11bb8a3ca --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Media/StorageProviderExtensions.cs @@ -0,0 +1,29 @@ +using Orchard.FileSystems.Media; + +namespace Orchard.MediaProcessing.Media { + public static class StorageProviderExtensions { + public static bool FileExists(this IStorageProvider storageProvider, string path) { + try { + storageProvider.GetFile(path); + return true; + } + catch {} + return false; + } + + public static void TryDeleteFolder(this IStorageProvider storageProvider, string path) { + try { + storageProvider.DeleteFolder(path); + } + catch {} + } + + public static IStorageFile OpenOrCreate(this IStorageProvider storageProvider, string path) { + try { + return storageProvider.CreateFile(path); + } + catch {} + return storageProvider.GetFile(path); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Migrations.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Migrations.cs new file mode 100644 index 000000000..4297d3d2f --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Migrations.cs @@ -0,0 +1,43 @@ +using Orchard.ContentManagement.MetaData; +using Orchard.Data.Migration; + +namespace Orchard.MediaProcessing { + public class Migrations : DataMigrationImpl { + public int Create() { + SchemaBuilder.CreateTable("ImageProfilePartRecord", + table => table + .Column("Name", c => c.WithLength(255)) + .ContentPartRecord() + ); + + ContentDefinitionManager.AlterTypeDefinition("ImageProfile", + cfg => cfg + .WithPart("ImageProfilePart") + .WithPart("CommonPart", p => p.WithSetting("OwnerEditorSettings.ShowOwnerEditor", "false")) + .WithPart("IdentityPart") + ); + + SchemaBuilder.CreateTable("FilterRecord", + table => table + .Column("Id", c => c.PrimaryKey().Identity()) + .Column("Category", c => c.WithLength(64)) + .Column("Type", c => c.WithLength(64)) + .Column("Description", c => c.WithLength(255)) + .Column("State", c => c.Unlimited()) + .Column("Position") + .Column("ImageProfilePartRecord_id") + ); + + SchemaBuilder.CreateTable("FileNameRecord", + table => table + .Column("Id", c => c.PrimaryKey().Identity()) + // Note: path and file name set to unlimited length, should have a sensible length + .Column("Path", c => c.Unlimited()) + .Column("FileName", c => c.Unlimited()) + .Column("ImageProfilePartRecord_id") + ); + + return 1; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Models/FileNameRecord.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Models/FileNameRecord.cs new file mode 100644 index 000000000..6eea1d105 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Models/FileNameRecord.cs @@ -0,0 +1,10 @@ +namespace Orchard.MediaProcessing.Models { + public class FileNameRecord { + public virtual int Id { get; set; } + public virtual string Path { get; set; } + public virtual string FileName { get; set; } + + // Parent property + public virtual ImageProfilePartRecord ImageProfilePartRecord { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Models/FilterRecord.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Models/FilterRecord.cs new file mode 100644 index 000000000..fd754f684 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Models/FilterRecord.cs @@ -0,0 +1,14 @@ +namespace Orchard.MediaProcessing.Models { + public class FilterRecord { + public virtual int Id { get; set; } + + public virtual string Description { get; set; } + public virtual string Category { get; set; } + public virtual string Type { get; set; } + public virtual int Position { get; set; } + public virtual string State { get; set; } + + // Parent property + public virtual ImageProfilePartRecord ImageProfilePartRecord { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Models/ImageProfilePart.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Models/ImageProfilePart.cs new file mode 100644 index 000000000..dc01757c3 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Models/ImageProfilePart.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using Orchard.ContentManagement; +using Orchard.ContentManagement.Aspects; + +namespace Orchard.MediaProcessing.Models { + public class ImageProfilePart : ContentPart { + + public string Name { + get { return Record.Name; } + set { Record.Name = value; } + } + + public DateTime? ModifiedUtc { + get { return this.As().ModifiedUtc; } + set { this.As().ModifiedUtc = value; } + } + + public IList Filters { + get { return Record.Filters; } + } + + public IList FileNames { + get { return Record.FileNames; } + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Models/ImageProfilePartRecord.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Models/ImageProfilePartRecord.cs new file mode 100644 index 000000000..a9fe78485 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Models/ImageProfilePartRecord.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Xml.Serialization; +using Orchard.ContentManagement.Records; +using Orchard.Data.Conventions; + +namespace Orchard.MediaProcessing.Models { + public class ImageProfilePartRecord : ContentPartRecord { + public ImageProfilePartRecord() { + Filters = new List(); + FileNames = new List(); + } + + public virtual string Name { get; set; } + + [CascadeAllDeleteOrphan, Aggregate] + [XmlArray("FilterRecords")] + public virtual IList Filters { get; set; } + + [CascadeAllDeleteOrphan, Aggregate] + [XmlArray("FileNameRecords")] + public virtual IList FileNames { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Module.txt b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Module.txt new file mode 100644 index 000000000..b7bcd7d06 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Module.txt @@ -0,0 +1,9 @@ +Name: Media Processing +AntiForgery: enabled +Author: The Orchard Team +Website: http://orchardproject.net +Version: 1.0 +OrchardVersion: 1.0 +Description: Module for processing Media e.g. image resizing +Category: Media +Dependencies: Orchard.Media, Orchard.Forms \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Orchard.MediaProcessing.csproj b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Orchard.MediaProcessing.csproj new file mode 100644 index 000000000..cf979d7b5 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Orchard.MediaProcessing.csproj @@ -0,0 +1,193 @@ + + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {08191FCD-7258-4F19-95FB-AEC3DE77B2EB} + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + Orchard.MediaProcessing + Orchard.MediaProcessing + v4.0 + false + + + 4.0 + + + + false + + + + + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + AllRules.ruleset + + + pdbonly + true + bin\ + TRACE + prompt + 4 + AllRules.ruleset + + + + + + + 3.5 + + + + + False + ..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll + + + + + + + + + + + + + + + + + + + {2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6} + Orchard.Framework + + + {9916839C-39FC-4CEB-A5AF-89CA7E87119F} + Orchard.Core + + + {642A49D7-8752-4177-80D6-BFBBCFAD3DE0} + Orchard.Forms + + + {D9A7B330-CD22-4DA1-A95A-8DE1982AD8EB} + Orchard.Media + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + + + + $(ProjectDir)\..\Manifests + + + + + + + + + + + + False + True + 45979 + / + + + False + True + http://orchard.codeplex.com + False + + + + + \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Placement.info b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Placement.info new file mode 100644 index 000000000..f91260a13 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Placement.info @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Properties/AssemblyInfo.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..971c9c725 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Orchard.ImageProcessing")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyProduct("Orchard")] +[assembly: AssemblyCopyright("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("8ac0ba94-3401-4a83-8d40-5a3aa569d4ff")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] + diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Providers/Filters/CropFilter.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Providers/Filters/CropFilter.cs new file mode 100644 index 000000000..0a77971b6 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Providers/Filters/CropFilter.cs @@ -0,0 +1,89 @@ +using System; +using System.Drawing; +using Orchard.DisplayManagement; +using Orchard.Forms.Services; +using Orchard.Localization; +using Orchard.MediaProcessing.Descriptors.Filter; +using Orchard.MediaProcessing.Services; + +namespace Orchard.MediaProcessing.Providers.Filters { + public class CropFilter : IImageFilterProvider { + public CropFilter() { + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + public void Describe(DescribeFilterContext describe) { + describe.For("Transform", T("Transform"), T("Transform")) + .Element("Crop", T("Crop"), T("Crops to a fixed height and width"), + ApplyFilter, + DisplayFilter, + "CropFilter" + ); + } + + public void ApplyFilter(FilterContext context) { + var newHeight = int.Parse(context.State.Height); + newHeight = newHeight > 0 ? newHeight : context.Image.Height; + var heightFactor = (float) context.Image.Height/newHeight; + + var newWidth = int.Parse(context.State.Width); + newWidth = newWidth > 0 ? newWidth : context.Image.Width; + var widthFactor = context.Image.Width/newWidth; + + if (widthFactor != heightFactor) { + if (widthFactor > heightFactor) { + newHeight = Convert.ToInt32(context.Image.Height/widthFactor); + } + else { + newWidth = Convert.ToInt32(context.Image.Width/heightFactor); + } + } + + var newImage = new Bitmap(newWidth, newHeight); + using (var graphics = Graphics.FromImage(newImage)) { + graphics.DrawImage(context.Image, 0, 0, new Rectangle(0, 0, newWidth, newHeight), GraphicsUnit.Pixel); + } + + context.Image = newImage; + } + + public LocalizedString DisplayFilter(FilterContext context) { + return T("Crop to {0}px high x {1}px wide", context.State.Height, context.State.Width); + } + } + + public class CropFilterForms : IFormProvider { + protected dynamic Shape { get; set; } + public Localizer T { get; set; } + + public CropFilterForms( + IShapeFactory shapeFactory) { + Shape = shapeFactory; + T = NullLocalizer.Instance; + } + + public void Describe(DescribeContext context) { + Func form = + shape => { + var f = Shape.Form( + Id: "ImageCropFilter", + _Height: Shape.Textbox( + Id: "height", Name: "Height", + Title: T("Height"), + Description: T("The height in pixels, 0 to allow any value."), + Classes: new[] {"text-small"}), + _Width: Shape.Textbox( + Id: "width", Name: "Width", + Title: T("Width"), + Description: T("The width in pixels, 0 to allow any value."), + Classes: new[] {"text-small"})); + + return f; + }; + + context.Form("CropFilter", form); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Providers/Filters/ImageFormatFilter.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Providers/Filters/ImageFormatFilter.cs new file mode 100644 index 000000000..2e7f40d56 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Providers/Filters/ImageFormatFilter.cs @@ -0,0 +1,92 @@ +using System; +using System.Drawing.Imaging; +using System.IO; +using System.Web.Mvc; +using Orchard.DisplayManagement; +using Orchard.Forms.Services; +using Orchard.Localization; +using Orchard.MediaProcessing.Descriptors.Filter; +using Orchard.MediaProcessing.Services; + +namespace Orchard.MediaProcessing.Providers.Filters { + public class ImageFormatFilter : IImageFilterProvider { + public ImageFormatFilter() { + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + public void Describe(DescribeFilterContext describe) { + describe.For("Transform", T("Transform"), T("Transform")) + .Element("ImageFormat", T("ImageFormat"), T("Changes the image file format"), + ApplyFilter, + DisplayFilter, + "ImageFormatFilter" + ); + } + + public void ApplyFilter(FilterContext context) { + context.ImageFormat = ImageFormatConverter.ToImageFormat((ImageFormats)Enum.Parse(typeof (ImageFormats), (string)context.State.ImageFormat)); + context.FilePath = Path.ChangeExtension(context.FilePath, context.ImageFormat.ToString().ToLower()); + } + + public LocalizedString DisplayFilter(FilterContext context) { + return T("Convert to {0}", context.State.ImageFormat.ToString()); + } + } + + public class ImageFormatFilterForms : IFormProvider { + protected dynamic Shape { get; set; } + public Localizer T { get; set; } + + public ImageFormatFilterForms( + IShapeFactory shapeFactory) { + Shape = shapeFactory; + T = NullLocalizer.Instance; + } + + public void Describe(DescribeContext context) { + Func form = + shape => { + var f = Shape.Form( + Id: "ImageFormatFilter", + _ImageFormat: Shape.SelectList( + Id: "imageformat", + Name: "ImageFormat" + )); + + foreach (var item in Enum.GetValues(typeof (ImageFormats))) { + var name = Enum.GetName(typeof (ImageFormats), item); + f._ImageFormat.Add(new SelectListItem {Value = item.ToString(), Text = name}); + } + + return f; + }; + + context.Form("ImageFormatFilter", form); + } + } + + public enum ImageFormats { + Bmp, + Gif, + Jpeg, + Png + } + + public class ImageFormatConverter { + public static ImageFormat ToImageFormat(ImageFormats format) { + switch (format) { + case ImageFormats.Bmp: + return ImageFormat.Bmp; + case ImageFormats.Gif: + return ImageFormat.Gif; + case ImageFormats.Jpeg: + return ImageFormat.Jpeg; + case ImageFormats.Png: + return ImageFormat.Png; + } + return ImageFormat.Jpeg; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Providers/Filters/ResizeFilter.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Providers/Filters/ResizeFilter.cs new file mode 100644 index 000000000..ec0c250c0 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Providers/Filters/ResizeFilter.cs @@ -0,0 +1,92 @@ +using System; +using System.Drawing; +using System.Drawing.Drawing2D; +using Orchard.DisplayManagement; +using Orchard.Forms.Services; +using Orchard.Localization; +using Orchard.MediaProcessing.Descriptors.Filter; +using Orchard.MediaProcessing.Services; + +namespace Orchard.MediaProcessing.Providers.Filters { + public class ResizeFilter : IImageFilterProvider { + public ResizeFilter() { + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + public void Describe(DescribeFilterContext describe) { + describe.For("Transform", T("Transform"), T("Transform")) + .Element("Resize", T("Resize"), T("Resizes to a fixed height and width"), + ApplyFilter, + DisplayFilter, + "ResizeFilter" + ); + } + + public void ApplyFilter(FilterContext context) { + var newHeight = int.Parse((string)context.State.Height); + newHeight = newHeight > 0 ? newHeight : context.Image.Height; + var heightFactor = (float) context.Image.Height/newHeight; + + var newWidth = int.Parse((string)context.State.Width); + newWidth = newWidth > 0 ? newWidth : context.Image.Width; + var widthFactor = context.Image.Width/newWidth; + + if (widthFactor != heightFactor) { + if (widthFactor > heightFactor) { + newHeight = Convert.ToInt32(context.Image.Height/widthFactor); + } + else { + newWidth = Convert.ToInt32(context.Image.Width/heightFactor); + } + } + + var newImage = new Bitmap(newWidth, newHeight); + using (var graphics = Graphics.FromImage(newImage)) { + graphics.CompositingQuality = CompositingQuality.HighSpeed; + graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; + graphics.CompositingMode = CompositingMode.SourceCopy; + graphics.DrawImage(context.Image, 0, 0, newWidth, newHeight); + } + + context.Image = newImage; + } + + public LocalizedString DisplayFilter(FilterContext context) { + return T("Resize to {0}px high x {1}px wide", context.State.Height, context.State.Width); + } + } + + public class ResizeFilterForms : IFormProvider { + protected dynamic Shape { get; set; } + public Localizer T { get; set; } + + public ResizeFilterForms( + IShapeFactory shapeFactory) { + Shape = shapeFactory; + T = NullLocalizer.Instance; + } + + public void Describe(DescribeContext context) { + Func form = + shape => { + var f = Shape.Form( + Id: "ImageResizeFilter", + _Height: Shape.Textbox( + Id: "height", Name: "Height", + Title: T("Height"), + Description: T("The height in pixels, 0 to allow any value."), + Classes: new[] {"text-small"}), + _Width: Shape.Textbox( + Id: "width", Name: "Width", + Title: T("Width"), + Description: T("The width in pixels, 0 to allow any value."), + Classes: new[] {"text-small"})); + return f; + }; + + context.Form("ResizeFilter", form); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Scripts/Web.config b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Scripts/Web.config new file mode 100644 index 000000000..770adfab5 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Scripts/Web.config @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/IImageFilterProvider.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/IImageFilterProvider.cs new file mode 100644 index 000000000..51aa16c95 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/IImageFilterProvider.cs @@ -0,0 +1,7 @@ +using Orchard.MediaProcessing.Descriptors.Filter; + +namespace Orchard.MediaProcessing.Services { + public interface IImageFilterProvider : IDependency { + void Describe(DescribeFilterContext describe); + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/IImageProcessingFileNameProvider.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/IImageProcessingFileNameProvider.cs new file mode 100644 index 000000000..35263bbdb --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/IImageProcessingFileNameProvider.cs @@ -0,0 +1,6 @@ +namespace Orchard.MediaProcessing.Services { + public interface IImageProcessingFileNameProvider : IDependency { + string GetFileName(string profile, string path); + void UpdateFileName(string profile, string path, string fileName); + } +} diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/IImageProcessingManager.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/IImageProcessingManager.cs new file mode 100644 index 000000000..f84b5a255 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/IImageProcessingManager.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using Orchard.MediaProcessing.Descriptors; +using Orchard.MediaProcessing.Descriptors.Filter; + +namespace Orchard.MediaProcessing.Services { + public interface IImageProcessingManager : IDependency { + IEnumerable> DescribeFilters(); + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/IImageProfileService.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/IImageProfileService.cs new file mode 100644 index 000000000..b8c64881f --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/IImageProfileService.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Orchard.MediaProcessing.Models; + +namespace Orchard.MediaProcessing.Services { + public interface IImageProfileService : IDependency { + ImageProfilePart GetImageProfile(int id); + ImageProfilePart GetImageProfileByName(string name); + IEnumerable GetAllImageProfiles(); + ImageProfilePart CreateImageProfile(string name); + void DeleteImageProfile(int id); + void MoveUp(int filterId); + void MoveDown(int filterId); + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/ImageProcessingFileNameProvider.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/ImageProcessingFileNameProvider.cs new file mode 100644 index 000000000..1895eaee2 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/ImageProcessingFileNameProvider.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Orchard.Caching; +using Orchard.Data; +using Orchard.MediaProcessing.Models; + +namespace Orchard.MediaProcessing.Services { + public class ImageProcessingFileNameProvider : IImageProcessingFileNameProvider { + private readonly IImageProfileService _imageProfileService; + private readonly ICacheManager _cacheManager; + private readonly ISignals _signals; + + public ImageProcessingFileNameProvider(IImageProfileService imageProfileService, ICacheManager cacheManager, ISignals signals) { + _imageProfileService = imageProfileService; + _cacheManager = cacheManager; + _signals = signals; + } + + public string GetFileName(string profile, string path) { + var fileNames = GetFileNames(profile); + string fileName; + if (!fileNames.TryGetValue(path, out fileName)) { + var profilePart = _imageProfileService.GetImageProfileByName(profile); + if (profilePart != null) { + var fileNameRecord = profilePart.FileNames.FirstOrDefault(f => f.Path == path); + if (fileNameRecord == null) { + return null; + } + fileNames.Add(path, fileNameRecord.FileName); + return fileNameRecord.FileName; + } + } + return fileName; + } + + public void UpdateFileName(string profile, string path, string fileName) { + var fileNames = GetFileNames(profile); + string existingFileName; + if (fileNames.TryGetValue(path, out existingFileName) && existingFileName == fileName) { + return; + } + fileNames[path] = fileName; + var profilePart = _imageProfileService.GetImageProfileByName(profile); + var fileNameRecord = profilePart.FileNames.FirstOrDefault(f => f.Path == path); + if (fileNameRecord == null) { + fileNameRecord = new FileNameRecord { + Path = path, + ImageProfilePartRecord = profilePart.Record + }; + profilePart.FileNames.Add(fileNameRecord); + } + fileNameRecord.FileName = fileName; + } + + private Dictionary GetFileNames(string profile) { + return _cacheManager.Get("MediaProcessing_" + profile, ctx => { + ctx.Monitor(_signals.When("MediaProcessing_" + profile + "_Saved")); + return new Dictionary(); + }); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/ImageProcessingManager.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/ImageProcessingManager.cs new file mode 100644 index 000000000..2b9ff615a --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/ImageProcessingManager.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Orchard.Localization; +using Orchard.MediaProcessing.Descriptors; +using Orchard.MediaProcessing.Descriptors.Filter; + +namespace Orchard.MediaProcessing.Services { + public class ImageProcessingManager : IImageProcessingManager { + private readonly IEnumerable _filterProviders; + + public ImageProcessingManager( + IEnumerable filterProviders) { + _filterProviders = filterProviders; + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + public IEnumerable> DescribeFilters() { + var context = new DescribeFilterContext(); + + foreach (var provider in _filterProviders) { + provider.Describe(context); + } + return context.Describe(); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/ImageProfileService.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/ImageProfileService.cs new file mode 100644 index 000000000..0eb628fe9 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Services/ImageProfileService.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using System.Linq; +using Orchard.ContentManagement; +using Orchard.Data; +using Orchard.Localization; +using Orchard.MediaProcessing.Models; + +namespace Orchard.MediaProcessing.Services { + public class ImageProfileService : IImageProfileService { + private readonly IContentManager _contentManager; + private readonly IRepository _filterRepository; + + public ImageProfileService(IContentManager contentManager, IRepository filterRepository) { + _contentManager = contentManager; + _filterRepository = filterRepository; + } + + public Localizer T { get; set; } + + public ImageProfilePart GetImageProfile(int id) { + return _contentManager.Get(id); + } + + public ImageProfilePart GetImageProfileByName(string name) { + return _contentManager.Query().Where(x => x.Name == name).Slice(0, 1).FirstOrDefault(); + } + + public IEnumerable GetAllImageProfiles() { + return _contentManager.Query().List(); + } + + public ImageProfilePart CreateImageProfile(string name) { + var contentItem = _contentManager.New("ImageProfile"); + var profile = contentItem.As(); + profile.Name = name; + + _contentManager.Create(contentItem); + + return profile; + } + + public void DeleteImageProfile(int id) { + var profile = _contentManager.Get(id); + + if (profile != null) { + _contentManager.Remove(profile); + } + } + + public void MoveUp(int filterId) { + var filter = _filterRepository.Get(filterId); + + // look for the previous action in order in same rule + var previous = _filterRepository.Table + .Where(x => x.Position < filter.Position && x.ImageProfilePartRecord.Id == filter.ImageProfilePartRecord.Id) + .OrderByDescending(x => x.Position) + .FirstOrDefault(); + + // nothing to do if already at the top + if (previous == null) { + return; + } + + // switch positions + var temp = previous.Position; + previous.Position = filter.Position; + filter.Position = temp; + } + + public void MoveDown(int filterId) { + var filter = _filterRepository.Get(filterId); + + // look for the next action in order in same rule + var next = _filterRepository.Table + .Where(x => x.Position > filter.Position && x.ImageProfilePartRecord.Id == filter.ImageProfilePartRecord.Id) + .OrderBy(x => x.Position) + .FirstOrDefault(); + + // nothing to do if already at the end + if (next == null) { + return; + } + + // switch positions + var temp = next.Position; + next.Position = filter.Position; + filter.Position = temp; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Shapes/MediaShapes.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Shapes/MediaShapes.cs new file mode 100644 index 000000000..734868f48 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Shapes/MediaShapes.cs @@ -0,0 +1,100 @@ +using System; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Net; +using System.Web; +using System.Web.Mvc; +using Orchard.DisplayManagement; +using Orchard.Environment; +using Orchard.FileSystems.Media; +using Orchard.Forms.Services; +using Orchard.Logging; +using Orchard.MediaProcessing.Descriptors.Filter; +using Orchard.MediaProcessing.Media; +using Orchard.MediaProcessing.Services; +using Orchard.Mvc.Html; +using Orchard.Utility.Extensions; + +namespace Orchard.MediaProcessing.Shapes { + public class MediaShapes : IDependency { + private readonly Work _storageProvider; + private readonly Work _fileNameProvider; + private readonly Work _profileService; + private readonly Work _processingManager; + private readonly Work _services; + + public MediaShapes(Work storageProvider, Work fileNameProvider, Work profileService, Work processingManager, Work services) { + _storageProvider = storageProvider; + _fileNameProvider = fileNameProvider; + _profileService = profileService; + _processingManager = processingManager; + _services = services; + Logger = NullLogger.Instance; + } + + public ILogger Logger { get; set; } + + [Shape] + public void ImageUrl(dynamic Display, TextWriter Output, string Profile, string Path) { + var filePath = _fileNameProvider.Value.GetFileName(Profile, Path); + if (string.IsNullOrEmpty(filePath)) { + try { + var profilePart = _profileService.Value.GetImageProfileByName(Profile); + if (profilePart == null) + return; + + var image = GetImage(Path); + var filterContext = new FilterContext {Image = image, ImageFormat = image.RawFormat, FilePath = _storageProvider.Value.Combine(Profile, CreateDefaultFileName(Path))}; + foreach (var filter in profilePart.Filters.OrderBy(f => f.Position)) { + var descriptor = _processingManager.Value.DescribeFilters().SelectMany(x => x.Descriptors).FirstOrDefault(x => x.Category == filter.Category && x.Type == filter.Type); + if (descriptor == null) + continue; + filterContext.State = FormParametersHelper.ToDynamic(filter.State); + descriptor.Filter(filterContext); + } + + _fileNameProvider.Value.UpdateFileName(Profile, Path, filterContext.FilePath); + + if (!filterContext.Saved) { + _storageProvider.Value.TryCreateFolder(profilePart.Name); + var newFile = _storageProvider.Value.OpenOrCreate(filterContext.FilePath); + using (var imageStream = newFile.OpenWrite()) { + filterContext.Image.Save(imageStream, filterContext.ImageFormat); + } + } + filePath = filterContext.FilePath; + } + catch (Exception ex) { + Logger.Error(ex, "An error occured while processing {0} for image {1}", Profile, Path); + return; + } + } + Output.Write(_storageProvider.Value.GetPublicUrl(filePath)); + } + + // TODO: Update this method once the storage provider has been updated + private Image GetImage(string path) { + // http://blob.storage-provider.net/my-image.jpg + if (Uri.IsWellFormedUriString(path, UriKind.Absolute)) { + var webClient = new WebClient(); + return new Bitmap(webClient.OpenRead(new Uri(path))); + } + // ~/Media/Default/images/my-image.jpg + if (VirtualPathUtility.IsAppRelative(path)) { + var webClient = new WebClient(); + return new Bitmap(webClient.OpenRead(new Uri(_services.Value.WorkContext.HttpContext.Request.Url, VirtualPathUtility.ToAbsolute(path)))); + } + // images/my-image.jpg + var file = _storageProvider.Value.GetFile(path); + return new Bitmap(file.OpenRead()); + } + + private static string CreateDefaultFileName(string path) { + var extention = Path.GetExtension(path); + var newPath = Path.ChangeExtension(path, ""); + newPath = newPath.Replace(@"/", "_"); + return newPath.ToSafeName() + extention; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Styles/Web.config b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Styles/Web.config new file mode 100644 index 000000000..770adfab5 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Styles/Web.config @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/ViewModels/AdminCreateViewModel.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/ViewModels/AdminCreateViewModel.cs new file mode 100644 index 000000000..00dbdedff --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/ViewModels/AdminCreateViewModel.cs @@ -0,0 +1,8 @@ +using System.ComponentModel.DataAnnotations; + +namespace Orchard.MediaProcessing.ViewModels { + public class AdminCreateViewModel { + [Required, StringLength(1024)] + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/ViewModels/AdminEditViewModel.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/ViewModels/AdminEditViewModel.cs new file mode 100644 index 000000000..e5994699f --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/ViewModels/AdminEditViewModel.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Orchard.MediaProcessing.ViewModels { + public class AdminEditViewModel { + public int Id { get; set; } + + [Required, StringLength(1024)] + public string Name { get; set; } + + public IEnumerable Filters { get; set; } + } + + public class FilterEntry { + public int FilterRecordId { get; set; } + public string Category { get; set; } + public string Type { get; set; } + public string DisplayText { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/ViewModels/AdminIndexViewModel.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/ViewModels/AdminIndexViewModel.cs new file mode 100644 index 000000000..da7e88208 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/ViewModels/AdminIndexViewModel.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Orchard.MediaProcessing.Models; + +namespace Orchard.MediaProcessing.ViewModels { + + public class AdminIndexViewModel { + public IList ImageProfiles { get; set; } + public AdminIndexOptions Options { get; set; } + public dynamic Pager { get; set; } + } + + public class ImageProfileEntry { + public ImageProfilePartRecord ImageProfile { get; set; } + public bool IsChecked { get; set; } + + public int ImageProfileId { get; set; } + public string Name { get; set; } + } + + public class AdminIndexOptions { + public string Search { get; set; } + public ImageProfilesFilter Filter { get; set; } + public ImageProfilesBulkAction BulkAction { get; set; } + } + + public enum ImageProfilesFilter { + All + } + + public enum ImageProfilesBulkAction { + None, + Delete + } +} diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/ViewModels/FilterAddViewModel.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/ViewModels/FilterAddViewModel.cs new file mode 100644 index 000000000..cbf109018 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/ViewModels/FilterAddViewModel.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Orchard.MediaProcessing.Descriptors; +using Orchard.MediaProcessing.Descriptors.Filter; + +namespace Orchard.MediaProcessing.ViewModels { + public class FilterAddViewModel { + public int Id { get; set; } + public IEnumerable> Filters { get; set; } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/ViewModels/FilterEditViewModel.cs b/src/Orchard.Web/Modules/Orchard.MediaProcessing/ViewModels/FilterEditViewModel.cs new file mode 100644 index 000000000..fbf147c07 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/ViewModels/FilterEditViewModel.cs @@ -0,0 +1,10 @@ +using Orchard.MediaProcessing.Descriptors.Filter; + +namespace Orchard.MediaProcessing.ViewModels { + public class FilterEditViewModel { + public int Id { get; set; } + public string Description { get; set; } + public FilterDescriptor Filter { get; set; } + public dynamic Form { get; set; } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Views/Admin/Edit.cshtml b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Views/Admin/Edit.cshtml new file mode 100644 index 000000000..e38a7caf8 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Views/Admin/Edit.cshtml @@ -0,0 +1,48 @@ +@model Orchard.MediaProcessing.ViewModels.AdminEditViewModel +@{ + Layout.Title = T("Edit Media Profile - {0}", Model.Name).Text; +} + +@using (Html.BeginFormAntiForgeryPost()) +{ + @Html.ValidationSummary() + +
+

@T("Filters")

+
+ +
+ @Html.ActionLink(T("Add a new Filter").Text, "Add", new { controller = "Filter", id = Model.Id }, new { @class = "button primaryAction" }) +
+ + + + + + + + + @foreach (var filter in Model.Filters) + { + + + + + } +
@T("Description") 
@filter.DisplayText + @Html.ActionLink(T("Edit").Text, "Edit", new { controller = "Filter", id = Model.Id, category = filter.Category, type = filter.Type, filterId = filter.FilterRecordId }) | + @Html.ActionLink(T("Delete").Text, "Delete", new { controller = "Filter", id = Model.Id, filterId = filter.FilterRecordId }, new { itemprop = "RemoveUrl UnsafeUrl" }) | + @if (filter != Model.Filters.First()) { + @Html.ActionLink(T("Up").Text, "Move", new { controller = "Admin", id = Model.Id, direction = "up", filterId = filter.FilterRecordId }) @:| + } + @if (filter != Model.Filters.Last()) { + @Html.ActionLink(T("Down").Text, "Move", new { controller = "Admin", id = Model.Id, direction = "down", filterId = filter.FilterRecordId }) + } + +
+ +
+ @*@Html.ActionLink(T("Preview").ToString(), "Preview", new { Model.Id }, new { @class = "button" })*@ + @Html.ActionLink(T("Close").ToString(), "Index", new { }, new { @class = "button" }) +
+} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Views/Admin/Index.cshtml b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Views/Admin/Index.cshtml new file mode 100644 index 000000000..289a939cb --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Views/Admin/Index.cshtml @@ -0,0 +1,68 @@ +@model Orchard.MediaProcessing.ViewModels.AdminIndexViewModel +@using Orchard.MediaProcessing.ViewModels +@{ + Layout.Title = T("Profiles").ToString(); + + var index = 0; + + var pageSizes = new List() { 10, 50, 100 }; + var defaultPageSize = WorkContext.CurrentSite.PageSize; + if(!pageSizes.Contains(defaultPageSize)) { + pageSizes.Add(defaultPageSize); + } +} + +@using (Html.BeginFormAntiForgeryPost()) { + @Html.ValidationSummary() +
@Html.ActionLink(T("Add a new Media Profile").ToString(), "Create", new { Area = "Contents", id = "ImageProfile", returnurl = HttpContext.Current.Request.RawUrl }, new { @class = "button primaryAction" })
+ +
+ + + +
+
+ + + + +
+
+ + + + + + + + + @foreach (var entry in Model.ImageProfiles) { + + + + + + index++; + } +
 ↓@T("Name") 
+ + + + @Html.ActionLink(entry.Name, "Edit", new { id = entry.ImageProfileId }) + + @Html.ActionLink(T("Properties").ToString(), "Edit", new { Area = "Contents", id = entry.ImageProfileId, returnurl = HttpContext.Current.Request.RawUrl }) | + @Html.ActionLink(T("Edit").ToString(), "Edit", new { id = entry.ImageProfileId }) | + @Html.ActionLink(T("Delete").ToString(), "Delete", new { id = entry.ImageProfileId }, new { itemprop = "RemoveUrl UnsafeUrl" }) | + @*@Html.ActionLink(T("Preview").ToString(), "Preview", new { id = entry.ImageProfileId })*@ +
+ @Display(Model.Pager) +
+} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Views/Admin/Preview.cshtml b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Views/Admin/Preview.cshtml new file mode 100644 index 000000000..e69de29bb diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Views/EditorTemplates/Parts.MediaProcessing.ImageProfilePart.cshtml b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Views/EditorTemplates/Parts.MediaProcessing.ImageProfilePart.cshtml new file mode 100644 index 000000000..b3ed77457 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Views/EditorTemplates/Parts.MediaProcessing.ImageProfilePart.cshtml @@ -0,0 +1,5 @@ +@model Orchard.MediaProcessing.Models.ImageProfilePart +
+ @Html.LabelFor(m => m.Name, T("Name")) + @Html.TextBoxFor(m => m.Name, new {@class = "text"}) +
\ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Views/Filter/Add.cshtml b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Views/Filter/Add.cshtml new file mode 100644 index 000000000..fb2807ff0 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Views/Filter/Add.cshtml @@ -0,0 +1,27 @@ +@model Orchard.MediaProcessing.ViewModels.FilterAddViewModel +@{ + Layout.Title = T("Add a Filter"); +} + +@if (!Model.Filters.Any()) { +

@T("There are no currently available filters")

+} + +@foreach (var filter in Model.Filters.OrderBy(x => x.Name.Text)) { +

@filter.Name

+ + + + + + + + + @foreach (var descriptor in filter.Descriptors) { + + + + + } +
@T("Name")@T("Description")
@Html.ActionLink(descriptor.Name.Text, "Edit", new { id = Model.Id, category = descriptor.Category, type = descriptor.Type })@descriptor.Description
+} diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Views/Filter/Edit.cshtml b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Views/Filter/Edit.cshtml new file mode 100644 index 000000000..1615be209 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Views/Filter/Edit.cshtml @@ -0,0 +1,31 @@ +@model Orchard.MediaProcessing.ViewModels.FilterEditViewModel +@{ + Layout.Title = T("Edit Filter - {0}", Model.Filter.Name); +} + +@Html.ValidationSummary() + +

@Model.Filter.Name

+@Model.Filter.Description + +@using (Html.BeginFormAntiForgeryPost()) { + +
+ @Html.LabelFor(m => m.Description, T("Description")) + @Html.TextBoxFor(m => m.Description, new { @class = "textMedium" }) + @T("You may optionally give a description to this filter, to be used in the dashboard screens.") +
+ + @* Render the dynamic form *@ + if (Model.Form != null) { +
+ @DisplayChildren(Model.Form) +
+ } + + @Display.TokenHint() + +
+ +
+} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Views/Web.config b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Views/Web.config new file mode 100644 index 000000000..b7d215131 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Views/Web.config @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.MediaProcessing/Web.config b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Web.config new file mode 100644 index 000000000..88b84a792 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.MediaProcessing/Web.config @@ -0,0 +1,41 @@ + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.sln b/src/Orchard.sln index 66ab88623..c140a08b1 100644 --- a/src/Orchard.sln +++ b/src/Orchard.sln @@ -146,7 +146,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UpgradeTo16", "Orchard.Web\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Workflows", "Orchard.Web\Modules\Orchard.Workflows\Orchard.Workflows.csproj", "{7059493C-8251-4764-9C1E-2368B8B485BC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Scripting.CSharp", "Orchard.Web\Modules\Orchard.Scripting.CSharp\Orchard.Scripting.CSharp.csproj", "{5D13EF34-8B39-4EC5-847F-E12892ACF841}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.MediaProcessing", "Orchard.Web\Modules\Orchard.MediaProcessing\Orchard.MediaProcessing.csproj", "{08191FCD-7258-4F19-95FB-AEC3DE77B2EB}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -819,16 +819,16 @@ Global {7059493C-8251-4764-9C1E-2368B8B485BC}.FxCop|Any CPU.ActiveCfg = Release|Any CPU {7059493C-8251-4764-9C1E-2368B8B485BC}.Release|Any CPU.ActiveCfg = Release|Any CPU {7059493C-8251-4764-9C1E-2368B8B485BC}.Release|Any CPU.Build.0 = Release|Any CPU - {5D13EF34-8B39-4EC5-847F-E12892ACF841}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU - {5D13EF34-8B39-4EC5-847F-E12892ACF841}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU - {5D13EF34-8B39-4EC5-847F-E12892ACF841}.Coverage|Any CPU.ActiveCfg = Release|Any CPU - {5D13EF34-8B39-4EC5-847F-E12892ACF841}.Coverage|Any CPU.Build.0 = Release|Any CPU - {5D13EF34-8B39-4EC5-847F-E12892ACF841}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5D13EF34-8B39-4EC5-847F-E12892ACF841}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5D13EF34-8B39-4EC5-847F-E12892ACF841}.FxCop|Any CPU.ActiveCfg = Release|Any CPU - {5D13EF34-8B39-4EC5-847F-E12892ACF841}.FxCop|Any CPU.Build.0 = Release|Any CPU - {5D13EF34-8B39-4EC5-847F-E12892ACF841}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5D13EF34-8B39-4EC5-847F-E12892ACF841}.Release|Any CPU.Build.0 = Release|Any CPU + {08191FCD-7258-4F19-95FB-AEC3DE77B2EB}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU + {08191FCD-7258-4F19-95FB-AEC3DE77B2EB}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU + {08191FCD-7258-4F19-95FB-AEC3DE77B2EB}.Coverage|Any CPU.ActiveCfg = Release|Any CPU + {08191FCD-7258-4F19-95FB-AEC3DE77B2EB}.Coverage|Any CPU.Build.0 = Release|Any CPU + {08191FCD-7258-4F19-95FB-AEC3DE77B2EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08191FCD-7258-4F19-95FB-AEC3DE77B2EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08191FCD-7258-4F19-95FB-AEC3DE77B2EB}.FxCop|Any CPU.ActiveCfg = Release|Any CPU + {08191FCD-7258-4F19-95FB-AEC3DE77B2EB}.FxCop|Any CPU.Build.0 = Release|Any CPU + {08191FCD-7258-4F19-95FB-AEC3DE77B2EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08191FCD-7258-4F19-95FB-AEC3DE77B2EB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -884,7 +884,7 @@ Global {3BD22132-D538-48C6-8854-F71333C798EB} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} {8A9FDB57-342D-49C2-BAFC-D885AAE5CC7C} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} {7059493C-8251-4764-9C1E-2368B8B485BC} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} - {5D13EF34-8B39-4EC5-847F-E12892ACF841} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} + {08191FCD-7258-4F19-95FB-AEC3DE77B2EB} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} {ABC826D4-2FA1-4F2F-87DE-E6095F653810} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA} {F112851D-B023-4746-B6B1-8D2E5AD8F7AA} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA} {6CB3EB30-F725-45C0-9742-42599BA8E8D2} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA}