diff --git a/src/ImageProcessor.Web/NET45/ImageProcessor.Web_NET45.csproj b/src/ImageProcessor.Web/NET45/ImageProcessor.Web_NET45.csproj index 40d4d8fdd..322a8fc66 100644 --- a/src/ImageProcessor.Web/NET45/ImageProcessor.Web_NET45.csproj +++ b/src/ImageProcessor.Web/NET45/ImageProcessor.Web_NET45.csproj @@ -67,6 +67,7 @@ + diff --git a/src/ImageProcessor.Web/NET45/Processors/Resize.cs b/src/ImageProcessor.Web/NET45/Processors/Resize.cs new file mode 100644 index 000000000..7d4a06ceb --- /dev/null +++ b/src/ImageProcessor.Web/NET45/Processors/Resize.cs @@ -0,0 +1,324 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) James South. +// Licensed under the Apache License, Version 2.0. +// +// +// Resizes an image to the given dimensions. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Web.Processors +{ + using System; + using System.Collections.Generic; + using System.Drawing; + using System.Linq; + using System.Text; + using System.Text.RegularExpressions; + using ImageProcessor.Core.Common.Extensions; + using ImageProcessor.Imaging; + using ImageProcessor.Processors; + + /// + /// Resizes an image to the given dimensions. + /// + public class Resize : IWebGraphicsProcessor + { + /// + /// The regular expression to search strings for. + /// + private static readonly Regex QueryRegex = new Regex(@"(width|height)=|(width|height)ratio=|mode=|anchor=|center=|upscale=", RegexOptions.Compiled); + + /// + /// The regular expression to search strings for the size attribute. + /// + private static readonly Regex SizeRegex = new Regex(@"(width|height)=\d+", RegexOptions.Compiled); + + /// + /// The regular expression to search strings for the ratio attribute. + /// + private static readonly Regex RatioRegex = new Regex(@"(width|height)ratio=\d+(.\d+)?", RegexOptions.Compiled); + + /// + /// The regular expression to search strings for the mode attribute. + /// + private static readonly Regex ModeRegex = new Regex(@"mode=(pad|stretch|crop|max)", RegexOptions.Compiled); + + /// + /// The regular expression to search strings for the anchor attribute. + /// + private static readonly Regex AnchorRegex = new Regex(@"anchor=(top|bottom|left|right|center)", RegexOptions.Compiled); + + /// + /// The regular expression to search strings for the center attribute. + /// + private static readonly Regex CenterRegex = new Regex(@"center=\d+(.\d+)?[,-]\d+(.\d+)", RegexOptions.Compiled); + + /// + /// The regular expression to search strings for the upscale attribute. + /// + private static readonly Regex UpscaleRegex = new Regex(@"upscale=false", RegexOptions.Compiled); + + /// + /// Initializes a new instance of the class. + /// + public Resize() + { + this.Processor = new ImageProcessor.Processors.Resize(); + } + + /// + /// Gets the regular expression to search strings for. + /// + public Regex RegexPattern + { + get + { + return QueryRegex; + } + } + + /// + /// Gets the order in which this processor is to be used in a chain. + /// + public int SortOrder { get; private set; } + + /// + /// Gets the associated graphics processor. + /// + public IGraphicsProcessor Processor { get; private set; } + + /// + /// The position in the original string where the first character of the captured substring was found. + /// + /// The query string to search. + /// + /// The zero-based starting position in the original string where the captured substring was found. + /// + public int MatchRegexIndex(string queryString) + { + int index = 0; + + // Set the sort order to max to allow filtering. + this.SortOrder = int.MaxValue; + + // First merge the matches so we can parse . + StringBuilder stringBuilder = new StringBuilder(); + + foreach (Match match in this.RegexPattern.Matches(queryString)) + { + if (match.Success) + { + if (index == 0) + { + // Set the index on the first instance only. + this.SortOrder = match.Index; + stringBuilder.Append(queryString); + } + + index += 1; + } + } + + // Match syntax + string toParse = stringBuilder.ToString(); + + Size size = this.ParseSize(toParse); + ResizeLayer resizeLayer = new ResizeLayer(size) + { + ResizeMode = this.ParseMode(toParse), + AnchorPosition = this.ParsePosition(toParse), + Upscale = !UpscaleRegex.IsMatch(toParse), + CenterCoordinates = this.ParseCoordinates(toParse), + }; + + this.Processor.DynamicParameter = resizeLayer; + + // Correctly parse any restrictions. + string restrictions; + this.Processor.Settings.TryGetValue("RestrictTo", out restrictions); + ((ImageProcessor.Processors.Resize)this.Processor).RestrictedSizes = this.ParseRestrictions(restrictions); + return this.SortOrder; + } + + /// + /// Returns the correct for the given string. + /// + /// + /// The input string containing the value to parse. + /// + /// + /// The . + /// + private Size ParseSize(string input) + { + const string Width = "width="; + const string Height = "height="; + const string WidthRatio = "widthratio="; + const string HeightRatio = "heightratio="; + Size size = new Size(); + + // First merge the matches so we can parse . + StringBuilder stringBuilder = new StringBuilder(); + foreach (Match match in SizeRegex.Matches(input)) + { + stringBuilder.Append(match.Value); + } + + // First cater for single dimensions. + string value = stringBuilder.ToString(); + + if (input.Contains(Width) && !input.Contains(Height)) + { + size = new Size(value.ToPositiveIntegerArray()[0], 0); + } + + if (input.Contains(Height) && !input.Contains(Width)) + { + size = new Size(0, value.ToPositiveIntegerArray()[0]); + } + + // Both dimensions supplied. + if (input.Contains(Height) && input.Contains(Width)) + { + int[] dimensions = value.ToPositiveIntegerArray(); + + // Check the order in which they have been supplied. + size = input.IndexOf(Width, StringComparison.Ordinal) < input.IndexOf(Height, StringComparison.Ordinal) + ? new Size(dimensions[0], dimensions[1]) + : new Size(dimensions[1], dimensions[0]); + } + + // Calculate any ratio driven sizes. + if (size.Width == 0 || size.Height == 0) + { + stringBuilder.Clear(); + foreach (Match match in RatioRegex.Matches(input)) + { + stringBuilder.Append(match.Value); + } + + value = stringBuilder.ToString(); + + // Replace 0 width + if (size.Width == 0 && size.Height > 0 && input.Contains(WidthRatio) && !input.Contains(HeightRatio)) + { + size.Width = (int)(value.ToPositiveFloatArray()[0] * size.Height); + } + + // Replace 0 height + if (size.Height == 0 && size.Width > 0 && input.Contains(HeightRatio) && !input.Contains(WidthRatio)) + { + size.Height = (int)(value.ToPositiveFloatArray()[0] * size.Width); + } + } + + return size; + } + + /// + /// Returns the correct for the given string. + /// + /// + /// The input string containing the value to parse. + /// + /// + /// The correct . + /// + private ResizeMode ParseMode(string input) + { + foreach (Match match in ModeRegex.Matches(input)) + { + // Split on = + string mode = match.Value.Split('=')[1]; + + switch (mode) + { + case "stretch": + return ResizeMode.Stretch; + case "crop": + return ResizeMode.Crop; + case "max": + return ResizeMode.Max; + default: + return ResizeMode.Pad; + } + } + + return ResizeMode.Pad; + } + + /// + /// Returns the correct for the given string. + /// + /// + /// The input string containing the value to parse. + /// + /// + /// The correct . + /// + private AnchorPosition ParsePosition(string input) + { + foreach (Match match in AnchorRegex.Matches(input)) + { + // Split on = + string anchor = match.Value.Split('=')[1]; + + switch (anchor) + { + case "top": + return AnchorPosition.Top; + case "bottom": + return AnchorPosition.Bottom; + case "left": + return AnchorPosition.Left; + case "right": + return AnchorPosition.Right; + default: + return AnchorPosition.Center; + } + } + + return AnchorPosition.Center; + } + + /// + /// Parses the coordinates. + /// + /// The input. + /// The array containing the coordinates + private float[] ParseCoordinates(string input) + { + float[] floats = { }; + + foreach (Match match in CenterRegex.Matches(input)) + { + floats = match.Value.ToPositiveFloatArray(); + } + + return floats; + } + + /// + /// Returns a of sizes to restrict resizing to. + /// + /// + /// The input. + /// + /// + /// The to restrict resizing to. + /// + private List ParseRestrictions(string input) + { + List sizes = new List(); + + if (!string.IsNullOrWhiteSpace(input)) + { + sizes.AddRange(input.Split(',').Select(this.ParseSize)); + } + + return sizes; + } + } +} diff --git a/src/ImageProcessor/ImageFactory.cs b/src/ImageProcessor/ImageFactory.cs index c0ac9062b..1112c2e22 100644 --- a/src/ImageProcessor/ImageFactory.cs +++ b/src/ImageProcessor/ImageFactory.cs @@ -365,7 +365,7 @@ namespace ImageProcessor { if (this.ShouldProcess) { - ResizeLayer layer = new ResizeLayer(size, Color.Transparent, ResizeMode.Max); + ResizeLayer layer = new ResizeLayer(size, ResizeMode.Max); return this.Resize(layer); } diff --git a/src/ImageProcessor/Imaging/Formats/FormatBase.cs b/src/ImageProcessor/Imaging/Formats/FormatBase.cs index de590aea2..0f317ba0c 100644 --- a/src/ImageProcessor/Imaging/Formats/FormatBase.cs +++ b/src/ImageProcessor/Imaging/Formats/FormatBase.cs @@ -23,7 +23,7 @@ namespace ImageProcessor.Imaging.Formats /// /// Gets the file header. /// - public abstract byte[] FileHeader { get; } + public abstract byte[][] FileHeaders { get; } /// /// Gets the list of file extensions. diff --git a/src/ImageProcessor/Imaging/Formats/FormatUtilities.cs b/src/ImageProcessor/Imaging/Formats/FormatUtilities.cs index d3b6f9c56..7a87ad400 100644 --- a/src/ImageProcessor/Imaging/Formats/FormatUtilities.cs +++ b/src/ImageProcessor/Imaging/Formats/FormatUtilities.cs @@ -43,11 +43,15 @@ namespace ImageProcessor.Imaging.Formats // ReSharper disable once LoopCanBeConvertedToQuery foreach (ISupportedImageFormat supportedImageFormat in supportedImageFormats) { - byte[] header = supportedImageFormat.FileHeader; - if (header.SequenceEqual(buffer.Take(header.Length))) + byte[][] headers = supportedImageFormat.FileHeaders; + + foreach (byte[] header in headers) { - stream.Position = 0; - return supportedImageFormat; + if (header.SequenceEqual(buffer.Take(header.Length))) + { + stream.Position = 0; + return supportedImageFormat; + } } } diff --git a/src/ImageProcessor/Imaging/Formats/ISupportedImageFormat.cs b/src/ImageProcessor/Imaging/Formats/ISupportedImageFormat.cs index 8ad4e0763..4296b1e58 100644 --- a/src/ImageProcessor/Imaging/Formats/ISupportedImageFormat.cs +++ b/src/ImageProcessor/Imaging/Formats/ISupportedImageFormat.cs @@ -23,7 +23,7 @@ namespace ImageProcessor.Imaging.Formats /// /// Gets the file header. /// - byte[] FileHeader { get; } + byte[][] FileHeaders { get; } /// /// Gets the list of file extensions. diff --git a/src/ImageProcessor/Imaging/ResizeLayer.cs b/src/ImageProcessor/Imaging/ResizeLayer.cs index 4fe81645b..dc5185922 100644 --- a/src/ImageProcessor/Imaging/ResizeLayer.cs +++ b/src/ImageProcessor/Imaging/ResizeLayer.cs @@ -26,10 +26,6 @@ namespace ImageProcessor.Imaging /// /// The containing the width and height to set the image to. /// - /// - /// The to set as the background color. - /// Used for image formats that do not support transparency (Default transparent) - /// /// /// The resize mode to apply to resized image. (Default ResizeMode.Pad) /// @@ -41,14 +37,12 @@ namespace ImageProcessor.Imaging /// public ResizeLayer( Size size, - Color? backgroundColor = null, ResizeMode resizeMode = ResizeMode.Pad, AnchorPosition anchorPosition = AnchorPosition.Center, bool upscale = true) { this.Size = size; this.Upscale = upscale; - this.BackgroundColor = backgroundColor ?? Color.Transparent; this.ResizeMode = resizeMode; this.AnchorPosition = anchorPosition; } @@ -60,11 +54,6 @@ namespace ImageProcessor.Imaging /// public Size Size { get; set; } - /// - /// Gets or sets the background color. - /// - public Color BackgroundColor { get; set; } - /// /// Gets or sets the resize mode. /// @@ -111,7 +100,6 @@ namespace ImageProcessor.Imaging return this.Size == resizeLayer.Size && this.ResizeMode == resizeLayer.ResizeMode && this.AnchorPosition == resizeLayer.AnchorPosition - && this.BackgroundColor == resizeLayer.BackgroundColor && this.Upscale == resizeLayer.Upscale; } @@ -126,8 +114,7 @@ namespace ImageProcessor.Imaging return this.Size.GetHashCode() + this.ResizeMode.GetHashCode() + this.AnchorPosition.GetHashCode() + - this.BackgroundColor.GetHashCode() + this.Upscale.GetHashCode(); } } -} +} \ No newline at end of file diff --git a/src/ImageProcessor/Processors/Resize.cs b/src/ImageProcessor/Processors/Resize.cs index 072848ee6..5cbfc2f92 100644 --- a/src/ImageProcessor/Processors/Resize.cs +++ b/src/ImageProcessor/Processors/Resize.cs @@ -18,10 +18,6 @@ namespace ImageProcessor.Processors using System.Drawing.Imaging; using System.Globalization; using System.Linq; - using System.Text; - using System.Text.RegularExpressions; - - using ImageProcessor.Core.Common.Extensions; using ImageProcessor.Imaging; #endregion @@ -30,58 +26,7 @@ namespace ImageProcessor.Processors /// public class Resize : IGraphicsProcessor { - /// - /// The regular expression to search strings for. - /// - private static readonly Regex QueryRegex = new Regex(@"(width|height)=|(width|height)ratio=|mode=|anchor=|center=|bgcolor=|upscale=", RegexOptions.Compiled); - - /// - /// The regular expression to search strings for the size attribute. - /// - private static readonly Regex SizeRegex = new Regex(@"(width|height)=\d+", RegexOptions.Compiled); - - /// - /// The regular expression to search strings for the ratio attribute. - /// - private static readonly Regex RatioRegex = new Regex(@"(width|height)ratio=\d+(.\d+)?", RegexOptions.Compiled); - - /// - /// The regular expression to search strings for the mode attribute. - /// - private static readonly Regex ModeRegex = new Regex(@"mode=(pad|stretch|crop|max)", RegexOptions.Compiled); - - /// - /// The regular expression to search strings for the anchor attribute. - /// - private static readonly Regex AnchorRegex = new Regex(@"anchor=(top|bottom|left|right|center)", RegexOptions.Compiled); - - /// - /// The regular expression to search strings for the center attribute. - /// - private static readonly Regex CenterRegex = new Regex(@"center=\d+(.\d+)?[,-]\d+(.\d+)", RegexOptions.Compiled); - - /// - /// The regular expression to search strings for the color attribute. - /// - private static readonly Regex ColorRegex = new Regex(@"bgcolor=(transparent|\d+,\d+,\d+,\d+|([0-9a-fA-F]{3}){1,2})", RegexOptions.Compiled); - - /// - /// The regular expression to search strings for the upscale attribute. - /// - private static readonly Regex UpscaleRegex = new Regex(@"upscale=false", RegexOptions.Compiled); - #region IGraphicsProcessor Members - /// - /// Gets the regular expression to search strings for. - /// - public Regex RegexPattern - { - get - { - return QueryRegex; - } - } - /// /// Gets or sets DynamicParameter. /// @@ -91,15 +36,6 @@ namespace ImageProcessor.Processors set; } - /// - /// Gets the order in which this processor is to be used in a chain. - /// - public int SortOrder - { - get; - private set; - } - /// /// Gets or sets any additional settings required by the processor. /// @@ -110,56 +46,9 @@ namespace ImageProcessor.Processors } /// - /// The position in the original string where the first character of the captured substring was found. + /// Gets or sets the list of sizes to restrict resizing methods to. /// - /// - /// The query string to search. - /// - /// - /// The zero-based starting position in the original string where the captured substring was found. - /// - public int MatchRegexIndex(string queryString) - { - int index = 0; - - // Set the sort order to max to allow filtering. - this.SortOrder = int.MaxValue; - - // First merge the matches so we can parse . - StringBuilder stringBuilder = new StringBuilder(); - - foreach (Match match in this.RegexPattern.Matches(queryString)) - { - if (match.Success) - { - if (index == 0) - { - // Set the index on the first instance only. - this.SortOrder = match.Index; - stringBuilder.Append(queryString); - } - - index += 1; - } - } - - // Match syntax - string toParse = stringBuilder.ToString(); - - Size size = this.ParseSize(toParse); - ResizeLayer resizeLayer = new ResizeLayer(size) - { - ResizeMode = this.ParseMode(toParse), - AnchorPosition = this.ParsePosition(toParse), - BackgroundColor = this.ParseColor(toParse), - Upscale = !UpscaleRegex.IsMatch(toParse), - CenterCoordinates = this.ParseCoordinates(toParse), - }; - - this.DynamicParameter = resizeLayer; - - return this.SortOrder; - } + public List RestrictedSizes { get; set; } /// /// Processes the image. @@ -177,19 +66,16 @@ namespace ImageProcessor.Processors int height = this.DynamicParameter.Size.Height ?? 0; ResizeMode mode = this.DynamicParameter.ResizeMode; AnchorPosition anchor = this.DynamicParameter.AnchorPosition; - Color backgroundColor = this.DynamicParameter.BackgroundColor; bool upscale = this.DynamicParameter.Upscale; float[] centerCoordinates = this.DynamicParameter.CenterCoordinates; int defaultMaxWidth; int defaultMaxHeight; - string restrictions; - this.Settings.TryGetValue("RestrictTo", out restrictions); + int.TryParse(this.Settings["MaxWidth"], NumberStyles.Any, CultureInfo.InvariantCulture, out defaultMaxWidth); int.TryParse(this.Settings["MaxHeight"], NumberStyles.Any, CultureInfo.InvariantCulture, out defaultMaxHeight); - List restrictedSizes = this.ParseRestrictions(restrictions); - return this.ResizeImage(factory, width, height, defaultMaxWidth, defaultMaxHeight, restrictedSizes, backgroundColor, mode, anchor, upscale, centerCoordinates); + return this.ResizeImage(factory, width, height, defaultMaxWidth, defaultMaxHeight, this.RestrictedSizes, mode, anchor, upscale, centerCoordinates); } #endregion @@ -215,9 +101,6 @@ namespace ImageProcessor.Processors /// /// A containing image resizing restrictions. /// - /// - /// The background color to pad the image with. - /// /// /// The mode with which to resize the image. /// @@ -240,7 +123,6 @@ namespace ImageProcessor.Processors int defaultMaxWidth, int defaultMaxHeight, List restrictedSizes, - Color backgroundColor, ResizeMode resizeMode = ResizeMode.Pad, AnchorPosition anchorPosition = AnchorPosition.Center, bool upscale = true, @@ -404,7 +286,7 @@ namespace ImageProcessor.Processors } // Restrict sizes - if (restrictedSizes.Any()) + if (restrictedSizes != null && restrictedSizes.Any()) { bool reject = true; foreach (Size restrictedSize in restrictedSizes) @@ -463,7 +345,6 @@ namespace ImageProcessor.Processors using (ImageAttributes wrapMode = new ImageAttributes()) { wrapMode.SetWrapMode(WrapMode.TileFlipXY); - graphics.Clear(backgroundColor); Rectangle destRect = new Rectangle(destinationX, destinationY, destinationWidth, destinationHeight); graphics.DrawImage(image, destRect, 0, 0, sourceWidth, sourceHeight, GraphicsUnit.Pixel, wrapMode); } @@ -484,222 +365,5 @@ namespace ImageProcessor.Processors return image; } - - /// - /// Returns the correct for the given string. - /// - /// - /// The input string containing the value to parse. - /// - /// - /// The . - /// - private Size ParseSize(string input) - { - const string Width = "width="; - const string Height = "height="; - const string WidthRatio = "widthratio="; - const string HeightRatio = "heightratio="; - Size size = new Size(); - - // First merge the matches so we can parse . - StringBuilder stringBuilder = new StringBuilder(); - foreach (Match match in SizeRegex.Matches(input)) - { - stringBuilder.Append(match.Value); - } - - // First cater for single dimensions. - string value = stringBuilder.ToString(); - - if (input.Contains(Width) && !input.Contains(Height)) - { - size = new Size(value.ToPositiveIntegerArray()[0], 0); - } - - if (input.Contains(Height) && !input.Contains(Width)) - { - size = new Size(0, value.ToPositiveIntegerArray()[0]); - } - - // Both dimensions supplied. - if (input.Contains(Height) && input.Contains(Width)) - { - int[] dimensions = value.ToPositiveIntegerArray(); - - // Check the order in which they have been supplied. - size = input.IndexOf(Width, StringComparison.Ordinal) < input.IndexOf(Height, StringComparison.Ordinal) - ? new Size(dimensions[0], dimensions[1]) - : new Size(dimensions[1], dimensions[0]); - } - - // Calculate any ratio driven sizes. - if (size.Width == 0 || size.Height == 0) - { - stringBuilder.Clear(); - foreach (Match match in RatioRegex.Matches(input)) - { - stringBuilder.Append(match.Value); - } - - value = stringBuilder.ToString(); - - // Replace 0 width - if (size.Width == 0 && size.Height > 0 && input.Contains(WidthRatio) && !input.Contains(HeightRatio)) - { - size.Width = (int)(value.ToPositiveFloatArray()[0] * size.Height); - } - - // Replace 0 height - if (size.Height == 0 && size.Width > 0 && input.Contains(HeightRatio) && !input.Contains(WidthRatio)) - { - size.Height = (int)(value.ToPositiveFloatArray()[0] * size.Width); - } - } - - return size; - } - - /// - /// Returns the correct for the given string. - /// - /// - /// The input string containing the value to parse. - /// - /// - /// The correct . - /// - private ResizeMode ParseMode(string input) - { - foreach (Match match in ModeRegex.Matches(input)) - { - // Split on = - string mode = match.Value.Split('=')[1]; - - switch (mode) - { - case "stretch": - return ResizeMode.Stretch; - case "crop": - return ResizeMode.Crop; - case "max": - return ResizeMode.Max; - default: - return ResizeMode.Pad; - } - } - - return ResizeMode.Pad; - } - - /// - /// Returns the correct for the given string. - /// - /// - /// The input string containing the value to parse. - /// - /// - /// The correct . - /// - private AnchorPosition ParsePosition(string input) - { - foreach (Match match in AnchorRegex.Matches(input)) - { - // Split on = - string anchor = match.Value.Split('=')[1]; - - switch (anchor) - { - case "top": - return AnchorPosition.Top; - case "bottom": - return AnchorPosition.Bottom; - case "left": - return AnchorPosition.Left; - case "right": - return AnchorPosition.Right; - default: - return AnchorPosition.Center; - } - } - - return AnchorPosition.Center; - } - - /// - /// Returns the correct for the given string. - /// - /// - /// The input string containing the value to parse. - /// - /// - /// The correct - /// - private Color ParseColor(string input) - { - foreach (Match match in ColorRegex.Matches(input)) - { - string value = match.Value.Split('=')[1]; - - if (value == "transparent") - { - return Color.Transparent; - } - - if (value.Contains(",")) - { - int[] split = value.ToPositiveIntegerArray(); - byte red = split[0].ToByte(); - byte green = split[1].ToByte(); - byte blue = split[2].ToByte(); - byte alpha = split[3].ToByte(); - - return Color.FromArgb(alpha, red, green, blue); - } - - // Split on color-hex - return ColorTranslator.FromHtml("#" + value); - } - - return Color.Transparent; - } - - /// - /// Returns a of sizes to restrict resizing to. - /// - /// - /// The input. - /// - /// - /// The to restrict resizing to. - /// - private List ParseRestrictions(string input) - { - List sizes = new List(); - - if (!string.IsNullOrWhiteSpace(input)) - { - sizes.AddRange(input.Split(',').Select(this.ParseSize)); - } - - return sizes; - } - - /// - /// Parses the coordinates. - /// - /// The input. - /// The array containing the coordinates - private float[] ParseCoordinates(string input) - { - float[] floats = { }; - - foreach (Match match in CenterRegex.Matches(input)) - { - floats = match.Value.ToPositiveFloatArray(); - } - - return floats; - } } -} +} \ No newline at end of file