private async Task DuplicateSmallerImageIfLargeAndSave(byte[] formFileContent, string name, int ifLargerThan, int constrainX, int constrainY) { using (var job = new ImageJob()) { var info = await ImageJob.GetImageInfo(new BytesSource(formFileContent)); if (info.ImageWidth > ifLargerThan) { GetEncoder(info, out IEncoderPreset encoder, out string expectedExt); var resized = await job.Decode(formFileContent) .ConstrainWithin((uint)constrainX, (uint)constrainY) .EncodeToBytes(encoder) .Finish() .InProcessAndDisposeAsync(); var result = resized.First.TryGetBytes(); if (result.HasValue) { var fileName = Path.GetFileName(name); var ext = Path.GetExtension(fileName); var newFileName = name.Replace(ext, "-sm" + expectedExt); await _blog.SaveFile(result.Value.Array, newFileName); } } } }
private async Task <string> EncodeToPreferredFormatAndSave(byte[] formFileContent, string name) { using (var job = new ImageJob()) { var info = await ImageJob.GetImageInfo(new BytesSource(formFileContent)); GetEncoder(info, out IEncoderPreset encoder, out string expectedExt); var resized = await job.Decode(formFileContent) .EncodeToBytes(encoder) .Finish() .InProcessAndDisposeAsync(); var result = resized.First.TryGetBytes(); if (result.HasValue) { var fileName = Path.GetFileName(name); var ext = Path.GetExtension(fileName); var newFileName = name.Replace(ext, expectedExt); return(await _blog.SaveFile(result.Value.Array, newFileName)); } } return(null); }
static async Task Main(string[] args) { // From https://github.com/imazen/imageflow-dotnet#getting-image-dimensions-and-format var imageBytes = Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII="); var info = await ImageJob.GetImageInfo(new BytesSource(imageBytes)); Console.WriteLine(info.ImageWidth); Console.Read(); }
public async void TestPresets() { using (var contentRoot = new TempContentRoot() .AddResource("images/fire.jpg", "TestFiles.fire-umbrella-small.jpg")) { var hostBuilder = new HostBuilder() .ConfigureWebHost(webHost => { // Add TestServer webHost.UseTestServer(); webHost.Configure(app => { app.UseImageflow(new ImageflowMiddlewareOptions() .SetMapWebRoot(false) // Maps / to ContentRootPath/images .MapPath("/", Path.Combine(contentRoot.PhysicalPath, "images")) .AddPreset(new PresetOptions("tiny", PresetPriority.OverrideQuery) .SetCommand("width", "2") .SetCommand("height", "1")) .AddPreset(new PresetOptions("small", PresetPriority.DefaultValues) .SetCommand("width", "30") .SetCommand("height", "20")) ); }); }); // Build and start the IHost using var host = await hostBuilder.StartAsync(); // Create an HttpClient to send requests to the TestServer using var client = host.GetTestClient(); using var presetValidResponse = await client.GetAsync("/fire.jpg?preset=small&height=35&mode=pad"); presetValidResponse.EnsureSuccessStatusCode(); var responseBytes = await presetValidResponse.Content.ReadAsByteArrayAsync(); var imageResults = await ImageJob.GetImageInfo(new BytesSource(responseBytes)); Assert.Equal(30, imageResults.ImageWidth); Assert.Equal(35, imageResults.ImageHeight); using var presetTinyResponse = await client.GetAsync("/fire.jpg?preset=tiny&height=35"); presetTinyResponse.EnsureSuccessStatusCode(); responseBytes = await presetTinyResponse.Content.ReadAsByteArrayAsync(); imageResults = await ImageJob.GetImageInfo(new BytesSource(responseBytes)); Assert.Equal(2, imageResults.ImageWidth); Assert.Equal(1, imageResults.ImageHeight); await host.StopAsync(CancellationToken.None); } }
public async void TestGetImageInfo() { var imageBytes = Convert.FromBase64String( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII="); var info = await ImageJob.GetImageInfo(new BytesSource(imageBytes)); Assert.Equal(info.ImageWidth, 1); Assert.Equal(info.ImageHeight, 1); Assert.Equal(info.PreferredExtension, "png"); Assert.Equal(info.PreferredMimeType, "image/png"); Assert.Equal(info.FrameDecodesInto, PixelFormat.Bgra_32); }
/// <summary> /// Checks if an uploaded image meets the minimum dimensions. Will throw a ImageTooSmallException if not. /// </summary> private static async Task CheckImageDimensionsAsync(Stream imageStream) { var info = await ImageJob.GetImageInfo(new BytesSource(Utilities.ConvertStreamToBytes(imageStream))); var orientation = info.ImageWidth >= info.ImageHeight ? ImageOrientation.Landscape : ImageOrientation.Portrait; var imageTooSmallErrorMessage = $"Image must be equal or bigger than 800px on the longest side. Detected size {info.ImageWidth}x{info.ImageHeight}"; switch (orientation) { case ImageOrientation.Landscape when info.ImageWidth < 800: throw new ImageTooSmallException(imageTooSmallErrorMessage); case ImageOrientation.Portrait when info.ImageHeight < 800: throw new ImageTooSmallException(imageTooSmallErrorMessage); } imageStream.Position = 0; }
private Task <Result <Image> > ConvertAndUpload(Image dbImage, FormFile uploadedFile) { return(GetBytes() .Ensure(AreDimensionsValid, $"Uploading image size must be at least {MinimumImageWidth}×{MinimumImageHeight} pixels and the width mustn't exceed two heights and vice versa") .Bind(Convert) .Bind(Upload)); Result <byte[]> GetBytes() { using var binaryReader = new BinaryReader(uploadedFile.OpenReadStream()); return(Result.Success(binaryReader.ReadBytes((int)uploadedFile.Length))); } async Task <bool> AreDimensionsValid(byte[] imageBytes) { var info = await ImageJob.GetImageInfo(new BytesSource(imageBytes)); return(MinimumImageWidth <= info.ImageWidth && MinimumImageHeight <= info.ImageHeight && info.ImageWidth / info.ImageHeight < 2 && info.ImageHeight / info.ImageWidth < 2); } async Task <Result <ImageSet> > Convert(byte[] imageBytes) { var imagesSet = new ImageSet(); using var imageJob = new ImageJob(); var jobResult = await imageJob.Decode(imageBytes) .Constrain(new Constraint(ConstraintMode.Within, ResizedLargeImageMaximumSideSize, ResizedLargeImageMaximumSideSize)) .Branch(f => f.ConstrainWithin(ResizedSmallImageMaximumSideSize, ResizedSmallImageMaximumSideSize).EncodeToBytes(new MozJpegEncoder(TargetJpegQuality, true))) .EncodeToBytes(new MozJpegEncoder(TargetJpegQuality, true)) .Finish().InProcessAsync(); imagesSet.SmallImage = GetImage(1); imagesSet.MainImage = GetImage(2); return(imagesSet.MainImage.Any() && imagesSet.SmallImage.Any() ? Result.Success(imagesSet) : Result.Failure <ImageSet>("Processing of the images failed")); byte[] GetImage(int index) { var encodeResult = jobResult?.TryGet(index); var bytes = encodeResult?.TryGetBytes(); return(bytes != null?bytes.Value.ToArray() : new byte[] { }); } } async Task <Result <Image> > Upload(ImageSet imageSet) { dbImage.Position = _dbContext.Images.Count(i => i.ReferenceId == dbImage.ReferenceId && i.ImageType == dbImage.ImageType); var entry = _dbContext.Images.Add(dbImage); await _dbContext.SaveChangesAsync(); var imageId = entry.Entity.Id; SetImageKeys(); var addToBucketResult = await AddImagesToBucket(); if (!addToBucketResult) { _dbContext.Images.Remove(entry.Entity); await _dbContext.SaveChangesAsync(); return(Result.Failure <Image>("Uploading of the image failed")); } _dbContext.Images.Update(entry.Entity); await _dbContext.SaveChangesAsync(); _dbContext.DetachEntry(entry.Entity); return(Result.Success(dbImage)); void SetImageKeys() { var basePartOfKey = $"{S3FolderName}/{dbImage.ServiceSupplierId}/{imageId}"; dbImage.Keys.MainImage = $"{basePartOfKey}-main.jpg"; dbImage.Keys.SmallImage = $"{basePartOfKey}-small.jpg"; } async Task <bool> AddImagesToBucket() { await using var largeStream = new MemoryStream(imageSet.MainImage); await using var smallStream = new MemoryStream(imageSet.SmallImage); var imageList = new List <(string key, Stream stream)> { (dbImage.Keys.MainImage, largeStream), (dbImage.Keys.SmallImage, smallStream) }; var resultList = await _amazonS3ClientService.Add(_bucketName, imageList); foreach (var result in resultList) { if (result.IsFailure) { var keyList = new List <string> { dbImage.Keys.MainImage, dbImage.Keys.SmallImage }; await _amazonS3ClientService.Delete(_bucketName, keyList); return(false); } } return(true); } } }
/// <summary> /// Images contain metadata that describes the photo to varying degrees. This method extracts the metadata /// and parses out the most interesting pieces we're interested in and assigns it to the image object so we can /// present the information to the user and use it to help with searches. /// </summary> /// <param name="image">The Image object to assign the metadata to.</param> /// <param name="imageBytes">The byte array containing the recently-uploaded image file to inspect for metadata.</param> /// <param name="overwriteImageProperties">Specifies whether or not to update image properties from metadata that already have values.</param> /// <param name="log">Optionally pass in an ILogger instance to enable internal logging</param> public static async Task ParseAndAssignImageMetadata(Image image, byte[] imageBytes, bool overwriteImageProperties, ILogger log = null) { var stopwatch = new Stopwatch(); stopwatch.Start(); // whilst image dimensions can be extracted from metadata in some cases, not in every case and this isn't acceptable var info = await ImageJob.GetImageInfo(new BytesSource(imageBytes)); image.Metadata.Width = Convert.ToInt32(info.ImageWidth); image.Metadata.Height = Convert.ToInt32(info.ImageHeight); using var imageStream = new MemoryStream(imageBytes); imageStream.Position = 0; var directories = ImageMetadataReader.ReadMetadata(imageStream); image.Metadata.TakenDate = GetImageDateTaken(directories); if (image.Metadata.TakenDate.HasValue) { // overwrite the image created date with when the photo was taken image.Created = image.Metadata.TakenDate.Value; } var iso = GetImageIso(directories); if (iso.HasValue) { image.Metadata.Iso = iso.Value; } if (!image.Credit.HasValue() || overwriteImageProperties) { var credit = GetImageCredit(directories); if (credit.HasValue()) { image.Credit = credit; } } var exifIfd0Directory = directories.OfType <ExifIfd0Directory>().FirstOrDefault(); if (exifIfd0Directory != null) { var make = exifIfd0Directory.Tags.SingleOrDefault(t => t.Type == ExifDirectoryBase.TagMake); if (make != null && make.Description.HasValue()) { image.Metadata.CameraMake = make.Description; } var model = exifIfd0Directory.Tags.SingleOrDefault(t => t.Type == ExifDirectoryBase.TagModel); if (model != null && model.Description.HasValue()) { image.Metadata.CameraModel = model.Description; } if (!image.Caption.HasValue() || overwriteImageProperties) { var imageDescription = exifIfd0Directory.Tags.SingleOrDefault(t => t.Type == ExifDirectoryBase.TagImageDescription); if (imageDescription != null && imageDescription.Description.HasValue()) { image.Caption = imageDescription.Description; } } } var exifSubIfdDirectory = directories.OfType <ExifSubIfdDirectory>().FirstOrDefault(); if (exifSubIfdDirectory != null) { var exposureTime = exifSubIfdDirectory.Tags.SingleOrDefault(t => t.Type == ExifDirectoryBase.TagExposureTime); // ReSharper disable once ConditionIsAlwaysTrueOrFalse -- wrong, can return null if (exposureTime != null && exposureTime.Description.HasValue()) { image.Metadata.ExposureTime = exposureTime.Description; } var aperture = exifSubIfdDirectory.Tags.SingleOrDefault(t => t.Type == ExifDirectoryBase.TagAperture); // ReSharper disable once ConditionIsAlwaysTrueOrFalse -- wrong, can return null if (aperture != null && aperture.Description.HasValue()) { image.Metadata.Aperture = aperture.Description; } var exposureBias = exifSubIfdDirectory.Tags.SingleOrDefault(t => t.Type == ExifDirectoryBase.TagExposureBias); // ReSharper disable once ConditionIsAlwaysTrueOrFalse -- wrong, can return null if (exposureBias != null && exposureBias.Description.HasValue()) { image.Metadata.ExposureBias = exposureBias.Description; } var meteringMode = exifSubIfdDirectory.Tags.SingleOrDefault(t => t.Type == ExifDirectoryBase.TagMeteringMode); // ReSharper disable once ConditionIsAlwaysTrueOrFalse -- wrong, can return null if (meteringMode != null && meteringMode.Description.HasValue()) { image.Metadata.MeteringMode = meteringMode.Description; } var flash = exifSubIfdDirectory.Tags.SingleOrDefault(t => t.Type == ExifDirectoryBase.TagFlash); // ReSharper disable once ConditionIsAlwaysTrueOrFalse -- wrong, can return null if (flash != null && flash.Description.HasValue()) { image.Metadata.Flash = flash.Description; } var focalLength = exifSubIfdDirectory.Tags.SingleOrDefault(t => t.Type == ExifDirectoryBase.TagFocalLength); // ReSharper disable once ConditionIsAlwaysTrueOrFalse -- wrong, can return null if (focalLength != null && focalLength.Description.HasValue()) { image.Metadata.FocalLength = focalLength.Description; } var lensMake = exifSubIfdDirectory.Tags.SingleOrDefault(t => t.Type == ExifDirectoryBase.TagLensMake); // ReSharper disable once ConditionIsAlwaysTrueOrFalse -- wrong, can return null if (lensMake != null && lensMake.Description.HasValue()) { image.Metadata.LensMake = lensMake.Description; } var lensModel = exifSubIfdDirectory.Tags.SingleOrDefault(t => t.Type == ExifDirectoryBase.TagLensModel); // ReSharper disable once ConditionIsAlwaysTrueOrFalse -- wrong, can return null if (lensModel != null && lensModel.Description.HasValue()) { image.Metadata.LensModel = lensModel.Description; } var whiteBalance = exifSubIfdDirectory.Tags.SingleOrDefault(t => t.Type == ExifDirectoryBase.TagWhiteBalance); // ReSharper disable once ConditionIsAlwaysTrueOrFalse -- wrong, can return null if (whiteBalance != null && whiteBalance.Description.HasValue()) { image.Metadata.WhiteBalance = whiteBalance.Description; } var whiteBalanceMode = exifSubIfdDirectory.Tags.SingleOrDefault(t => t.Type == ExifDirectoryBase.TagWhiteBalanceMode); // ReSharper disable once ConditionIsAlwaysTrueOrFalse -- wrong, can return null if (whiteBalanceMode != null && whiteBalanceMode.Description.HasValue()) { image.Metadata.WhiteBalanceMode = whiteBalanceMode.Description; } } var gpsDirectory = directories.OfType <GpsDirectory>().FirstOrDefault(); var location = gpsDirectory?.GetGeoLocation(); if (location != null) { image.Metadata.LocationLatitude = location.Latitude; image.Metadata.LocationLongitude = location.Longitude; } var iptcDirectory = directories.OfType <IptcDirectory>().FirstOrDefault(); if (iptcDirectory != null) { var objectName = iptcDirectory.Tags.SingleOrDefault(t => t.Type == IptcDirectory.TagObjectName); if (objectName != null && objectName.Description.HasValue()) { image.Name = Utilities.TidyImageName(objectName.Description); } var keywords = iptcDirectory.GetKeywords(); if (keywords != null) { foreach (var keyword in keywords.Where(k => k.HasValue())) { image.TagsCsv = Utilities.AddTagToCsv(image.TagsCsv, keyword); } } if (!image.Metadata.Location.HasValue() || overwriteImageProperties) { var objectLocation = iptcDirectory.Tags.SingleOrDefault(t => t.Type == IptcDirectory.TagSubLocation); if (objectLocation != null && objectLocation.Description.HasValue()) { image.Metadata.Location = objectLocation.Description; } } if (!image.Metadata.City.HasValue() || overwriteImageProperties) { var objectCity = iptcDirectory.Tags.SingleOrDefault(t => t.Type == IptcDirectory.TagCity); if (objectCity != null && objectCity.Description.HasValue()) { image.Metadata.City = objectCity.Description; } } if (!image.Metadata.State.HasValue() || overwriteImageProperties) { var objectState = iptcDirectory.Tags.SingleOrDefault(t => t.Type == IptcDirectory.TagProvinceOrState); if (objectState != null && objectState.Description.HasValue()) { image.Metadata.State = objectState.Description; } } if (!image.Metadata.Country.HasValue() || overwriteImageProperties) { var objectCountry = iptcDirectory.Tags.SingleOrDefault(t => t.Type == IptcDirectory.TagCountryOrPrimaryLocationName); if (objectCountry != null && objectCountry.Description.HasValue()) { image.Metadata.Country = objectCountry.Description; } } } var xmpDirectory = directories.OfType <XmpDirectory>().FirstOrDefault(); if (xmpDirectory != null) { // see if there's any tags (subjects in xmp) // this will just add tags, so if we're re-processing, we won't lose any manually-entered ones // or cause duplications. var subjects = xmpDirectory.XmpMeta.Properties.Where(q => q.Path != null && q.Path.StartsWith("dc:subject") && !string.IsNullOrEmpty(q.Value)); foreach (var subject in subjects) { image.TagsCsv = Utilities.AddTagToCsv(image.TagsCsv, subject.Value); } // sometimes we have no camera info, but sometimes something can be inferred from lens profile info var lensProfileFilename = xmpDirectory.XmpMeta.GetPropertyString("http://ns.adobe.com/camera-raw-settings/1.0/", "crs:LensProfileFilename"); if (!string.IsNullOrEmpty(lensProfileFilename) && string.IsNullOrEmpty(image.Metadata.CameraModel)) { var camera = lensProfileFilename.Substring(0, lensProfileFilename.IndexOf(" (", StringComparison.Ordinal)); image.Metadata.CameraModel = camera; } var lensProfileName = xmpDirectory.XmpMeta.GetPropertyString("http://ns.adobe.com/camera-raw-settings/1.0/", "crs:LensProfileName"); if (!string.IsNullOrEmpty(lensProfileName) && string.IsNullOrEmpty(image.Metadata.LensModel)) { var lens = Regex.Match(lensProfileName, @"\((.*?)\)", RegexOptions.Compiled); if (lens.Success && lens.Value.Contains("(") && lens.Value.Contains(")")) { image.Metadata.LensModel = lens.Value.TrimStart('(').TrimEnd(')'); } } } image.Metadata.DateLastProcessed = DateTime.Now; stopwatch.Stop(); log?.Information($"LB.PhotoGalleries.Worker.MetadataUtils.ParseAndAssignImageMetadata() - Processed metadata for {image.Id} in {stopwatch.ElapsedMilliseconds}ms"); }
private async Task <Result> AddOrReplace(IFormFile file, string fileName, AgentContext agentContext) { return(await Validate() .Bind(Upload) .Tap(StoreLink)); async Task <Result <byte[]> > Validate() { return(await Result.Success() .Ensure(() => file != default, "Could not get the file") .Ensure(() => ImageExtensions.Contains(Path.GetExtension(file?.FileName)?.ToLowerInvariant()), "The file must have extension of a jpeg image") .Map(GetImageBytes) .Bind(ValidateImage)); async Task <byte[]> GetImageBytes() { await using var stream = file.OpenReadStream(); using var binaryReader = new BinaryReader(stream); return(binaryReader.ReadBytes((int)file.Length)); } async Task <Result <byte[]> > ValidateImage(byte[] imageBytes) { // Here should be checks like aspect ratio, for now only resolution checks var imageInfo = await ImageJob.GetImageInfo(new BytesSource(imageBytes)); return(Result.Success() .Ensure(() => imageInfo.ImageWidth >= MinimumWidth && imageInfo.ImageWidth <= MaximumWidth, $"Image width must be in range from {MinimumWidth} to {MaximumWidth}") .Ensure(() => imageInfo.ImageHeight >= MinimumHeight && imageInfo.ImageHeight <= MaximumHeight, $"Image height must be in range from {MinimumWidth} to {MaximumWidth}") .Map(() => imageBytes)); } } async Task <Result <string> > Upload(byte[] imageBytes) { await using var stream = new MemoryStream(imageBytes); return(await _amazonS3ClientService.Add(_bucketName, GetKey(agentContext.AgencyId, fileName), stream, S3CannedACL.PublicRead)); } async Task StoreLink(string url) { var oldUploadedImage = await _edoContext.UploadedImages .SingleOrDefaultAsync(i => i.AgencyId == agentContext.AgentId && i.FileName == fileName); if (oldUploadedImage == null) { var now = _dateTimeProvider.UtcNow(); var newUploadedImage = new UploadedImage { AgencyId = agentContext.AgencyId, Url = url, FileName = fileName, Created = now, Updated = now }; _edoContext.Add(newUploadedImage); } else { oldUploadedImage.Updated = _dateTimeProvider.UtcNow(); oldUploadedImage.Url = url; _edoContext.Update(oldUploadedImage); } await _edoContext.SaveChangesAsync(); } }