2019-08-18 05:21:27 -04:00
|
|
|
// Copyright 2019 The Hugo Authors. All rights reserved.
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
|
|
|
package images
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2019-10-20 04:39:00 -04:00
|
|
|
"image/color"
|
2019-08-18 05:21:27 -04:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
|
2019-08-26 13:12:41 -04:00
|
|
|
"github.com/disintegration/gift"
|
|
|
|
|
2019-08-18 05:21:27 -04:00
|
|
|
"github.com/mitchellh/mapstructure"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
defaultJPEGQuality = 75
|
|
|
|
defaultResampleFilter = "box"
|
2019-10-20 04:39:00 -04:00
|
|
|
defaultBgColor = "ffffff"
|
2019-08-18 05:21:27 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2019-08-26 13:12:41 -04:00
|
|
|
imageFormats = map[string]Format{
|
|
|
|
".jpg": JPEG,
|
|
|
|
".jpeg": JPEG,
|
|
|
|
".png": PNG,
|
|
|
|
".tif": TIFF,
|
|
|
|
".tiff": TIFF,
|
|
|
|
".bmp": BMP,
|
|
|
|
".gif": GIF,
|
2019-08-18 05:21:27 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Add or increment if changes to an image format's processing requires
|
|
|
|
// re-generation.
|
2019-08-26 13:12:41 -04:00
|
|
|
imageFormatsVersions = map[Format]int{
|
|
|
|
PNG: 2, // Floyd Steinberg dithering
|
2019-08-18 05:21:27 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Increment to mark all processed images as stale. Only use when absolutely needed.
|
|
|
|
// See the finer grained smartCropVersionNumber and imageFormatsVersions.
|
|
|
|
mainImageVersionNumber = 0
|
|
|
|
)
|
|
|
|
|
2019-08-26 13:12:41 -04:00
|
|
|
var anchorPositions = map[string]gift.Anchor{
|
|
|
|
strings.ToLower("Center"): gift.CenterAnchor,
|
|
|
|
strings.ToLower("TopLeft"): gift.TopLeftAnchor,
|
|
|
|
strings.ToLower("Top"): gift.TopAnchor,
|
|
|
|
strings.ToLower("TopRight"): gift.TopRightAnchor,
|
|
|
|
strings.ToLower("Left"): gift.LeftAnchor,
|
|
|
|
strings.ToLower("Right"): gift.RightAnchor,
|
|
|
|
strings.ToLower("BottomLeft"): gift.BottomLeftAnchor,
|
|
|
|
strings.ToLower("Bottom"): gift.BottomAnchor,
|
|
|
|
strings.ToLower("BottomRight"): gift.BottomRightAnchor,
|
2019-08-18 05:21:27 -04:00
|
|
|
}
|
|
|
|
|
2019-08-26 13:12:41 -04:00
|
|
|
var imageFilters = map[string]gift.Resampling{
|
|
|
|
|
|
|
|
strings.ToLower("NearestNeighbor"): gift.NearestNeighborResampling,
|
|
|
|
strings.ToLower("Box"): gift.BoxResampling,
|
|
|
|
strings.ToLower("Linear"): gift.LinearResampling,
|
|
|
|
strings.ToLower("Hermite"): hermiteResampling,
|
|
|
|
strings.ToLower("MitchellNetravali"): mitchellNetravaliResampling,
|
|
|
|
strings.ToLower("CatmullRom"): catmullRomResampling,
|
|
|
|
strings.ToLower("BSpline"): bSplineResampling,
|
|
|
|
strings.ToLower("Gaussian"): gaussianResampling,
|
|
|
|
strings.ToLower("Lanczos"): gift.LanczosResampling,
|
|
|
|
strings.ToLower("Hann"): hannResampling,
|
|
|
|
strings.ToLower("Hamming"): hammingResampling,
|
|
|
|
strings.ToLower("Blackman"): blackmanResampling,
|
|
|
|
strings.ToLower("Bartlett"): bartlettResampling,
|
|
|
|
strings.ToLower("Welch"): welchResampling,
|
|
|
|
strings.ToLower("Cosine"): cosineResampling,
|
2019-08-18 05:21:27 -04:00
|
|
|
}
|
|
|
|
|
2019-08-26 13:12:41 -04:00
|
|
|
func ImageFormatFromExt(ext string) (Format, bool) {
|
2019-08-18 05:21:27 -04:00
|
|
|
f, found := imageFormats[ext]
|
|
|
|
return f, found
|
|
|
|
}
|
|
|
|
|
2019-10-20 04:39:00 -04:00
|
|
|
func DecodeConfig(m map[string]interface{}) (ImagingConfig, error) {
|
2019-08-18 05:21:27 -04:00
|
|
|
var i Imaging
|
2019-10-20 04:39:00 -04:00
|
|
|
var ic ImagingConfig
|
2019-08-18 05:21:27 -04:00
|
|
|
if err := mapstructure.WeakDecode(m, &i); err != nil {
|
2019-10-20 04:39:00 -04:00
|
|
|
return ic, err
|
2019-08-18 05:21:27 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if i.Quality == 0 {
|
|
|
|
i.Quality = defaultJPEGQuality
|
|
|
|
} else if i.Quality < 0 || i.Quality > 100 {
|
2019-10-20 04:39:00 -04:00
|
|
|
return ic, errors.New("JPEG quality must be a number between 1 and 100")
|
|
|
|
}
|
|
|
|
|
|
|
|
if i.BgColor != "" {
|
|
|
|
i.BgColor = strings.TrimPrefix(i.BgColor, "#")
|
|
|
|
} else {
|
|
|
|
i.BgColor = defaultBgColor
|
|
|
|
}
|
|
|
|
var err error
|
|
|
|
ic.BgColor, err = hexStringToColor(i.BgColor)
|
|
|
|
if err != nil {
|
|
|
|
return ic, err
|
2019-08-18 05:21:27 -04:00
|
|
|
}
|
|
|
|
|
2019-08-26 13:12:41 -04:00
|
|
|
if i.Anchor == "" || strings.EqualFold(i.Anchor, smartCropIdentifier) {
|
|
|
|
i.Anchor = smartCropIdentifier
|
2019-08-18 05:21:27 -04:00
|
|
|
} else {
|
|
|
|
i.Anchor = strings.ToLower(i.Anchor)
|
|
|
|
if _, found := anchorPositions[i.Anchor]; !found {
|
2019-10-20 04:39:00 -04:00
|
|
|
return ic, errors.New("invalid anchor value in imaging config")
|
2019-08-18 05:21:27 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if i.ResampleFilter == "" {
|
|
|
|
i.ResampleFilter = defaultResampleFilter
|
|
|
|
} else {
|
|
|
|
filter := strings.ToLower(i.ResampleFilter)
|
|
|
|
_, found := imageFilters[filter]
|
|
|
|
if !found {
|
2019-10-20 04:39:00 -04:00
|
|
|
return ic, fmt.Errorf("%q is not a valid resample filter", filter)
|
2019-08-18 05:21:27 -04:00
|
|
|
}
|
|
|
|
i.ResampleFilter = filter
|
|
|
|
}
|
|
|
|
|
2019-08-29 04:18:51 -04:00
|
|
|
if strings.TrimSpace(i.Exif.IncludeFields) == "" && strings.TrimSpace(i.Exif.ExcludeFields) == "" {
|
|
|
|
// Don't change this for no good reason. Please don't.
|
|
|
|
i.Exif.ExcludeFields = "GPS|Exif|Exposure[M|P|B]|Contrast|Resolution|Sharp|JPEG|Metering|Sensing|Saturation|ColorSpace|Flash|WhiteBalance"
|
|
|
|
}
|
|
|
|
|
2019-10-20 04:39:00 -04:00
|
|
|
ic.Cfg = i
|
|
|
|
|
|
|
|
return ic, nil
|
2019-08-18 05:21:27 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func DecodeImageConfig(action, config string, defaults Imaging) (ImageConfig, error) {
|
|
|
|
var (
|
|
|
|
c ImageConfig
|
|
|
|
err error
|
|
|
|
)
|
|
|
|
|
|
|
|
c.Action = action
|
|
|
|
|
|
|
|
if config == "" {
|
|
|
|
return c, errors.New("image config cannot be empty")
|
|
|
|
}
|
|
|
|
|
|
|
|
parts := strings.Fields(config)
|
|
|
|
for _, part := range parts {
|
|
|
|
part = strings.ToLower(part)
|
|
|
|
|
2019-08-26 13:12:41 -04:00
|
|
|
if part == smartCropIdentifier {
|
|
|
|
c.AnchorStr = smartCropIdentifier
|
2019-08-18 05:21:27 -04:00
|
|
|
} else if pos, ok := anchorPositions[part]; ok {
|
|
|
|
c.Anchor = pos
|
|
|
|
c.AnchorStr = part
|
|
|
|
} else if filter, ok := imageFilters[part]; ok {
|
|
|
|
c.Filter = filter
|
|
|
|
c.FilterStr = part
|
2019-10-20 04:39:00 -04:00
|
|
|
} else if part[0] == '#' {
|
|
|
|
c.BgColorStr = part[1:]
|
|
|
|
c.BgColor, err = hexStringToColor(c.BgColorStr)
|
|
|
|
if err != nil {
|
|
|
|
return c, err
|
|
|
|
}
|
2019-08-18 05:21:27 -04:00
|
|
|
} else if part[0] == 'q' {
|
|
|
|
c.Quality, err = strconv.Atoi(part[1:])
|
|
|
|
if err != nil {
|
|
|
|
return c, err
|
|
|
|
}
|
|
|
|
if c.Quality < 1 || c.Quality > 100 {
|
|
|
|
return c, errors.New("quality ranges from 1 to 100 inclusive")
|
|
|
|
}
|
|
|
|
} else if part[0] == 'r' {
|
|
|
|
c.Rotate, err = strconv.Atoi(part[1:])
|
|
|
|
if err != nil {
|
|
|
|
return c, err
|
|
|
|
}
|
|
|
|
} else if strings.Contains(part, "x") {
|
|
|
|
widthHeight := strings.Split(part, "x")
|
|
|
|
if len(widthHeight) <= 2 {
|
|
|
|
first := widthHeight[0]
|
|
|
|
if first != "" {
|
|
|
|
c.Width, err = strconv.Atoi(first)
|
|
|
|
if err != nil {
|
|
|
|
return c, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(widthHeight) == 2 {
|
|
|
|
second := widthHeight[1]
|
|
|
|
if second != "" {
|
|
|
|
c.Height, err = strconv.Atoi(second)
|
|
|
|
if err != nil {
|
|
|
|
return c, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return c, errors.New("invalid image dimensions")
|
|
|
|
}
|
2019-09-21 10:50:27 -04:00
|
|
|
} else if f, ok := ImageFormatFromExt("." + part); ok {
|
|
|
|
c.TargetFormat = f
|
2019-08-18 05:21:27 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.Width == 0 && c.Height == 0 {
|
|
|
|
return c, errors.New("must provide Width or Height")
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.FilterStr == "" {
|
|
|
|
c.FilterStr = defaults.ResampleFilter
|
|
|
|
c.Filter = imageFilters[c.FilterStr]
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.AnchorStr == "" {
|
|
|
|
c.AnchorStr = defaults.Anchor
|
2019-08-26 13:12:41 -04:00
|
|
|
if !strings.EqualFold(c.AnchorStr, smartCropIdentifier) {
|
2019-08-18 05:21:27 -04:00
|
|
|
c.Anchor = anchorPositions[c.AnchorStr]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return c, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ImageConfig holds configuration to create a new image from an existing one, resize etc.
|
|
|
|
type ImageConfig struct {
|
2019-09-21 10:50:27 -04:00
|
|
|
// This defines the output format of the output image. It defaults to the source format
|
|
|
|
TargetFormat Format
|
|
|
|
|
2019-08-18 05:21:27 -04:00
|
|
|
Action string
|
|
|
|
|
2019-08-26 13:12:41 -04:00
|
|
|
// If set, this will be used as the key in filenames etc.
|
|
|
|
Key string
|
|
|
|
|
2019-08-18 05:21:27 -04:00
|
|
|
// Quality ranges from 1 to 100 inclusive, higher is better.
|
|
|
|
// This is only relevant for JPEG images.
|
|
|
|
// Default is 75.
|
|
|
|
Quality int
|
|
|
|
|
|
|
|
// Rotate rotates an image by the given angle counter-clockwise.
|
|
|
|
// The rotation will be performed first.
|
|
|
|
Rotate int
|
|
|
|
|
2019-10-20 04:39:00 -04:00
|
|
|
// Used to fill any transparency.
|
|
|
|
// When set in site config, it's used when converting to a format that does
|
|
|
|
// not support transparency.
|
|
|
|
// When set per image operation, it's used even for formats that does support
|
|
|
|
// transparency.
|
|
|
|
BgColor color.Color
|
|
|
|
BgColorStr string
|
|
|
|
|
2019-08-18 05:21:27 -04:00
|
|
|
Width int
|
|
|
|
Height int
|
|
|
|
|
2019-08-26 13:12:41 -04:00
|
|
|
Filter gift.Resampling
|
2019-08-18 05:21:27 -04:00
|
|
|
FilterStr string
|
|
|
|
|
2019-08-26 13:12:41 -04:00
|
|
|
Anchor gift.Anchor
|
2019-08-18 05:21:27 -04:00
|
|
|
AnchorStr string
|
|
|
|
}
|
|
|
|
|
2019-08-26 13:12:41 -04:00
|
|
|
func (i ImageConfig) GetKey(format Format) string {
|
|
|
|
if i.Key != "" {
|
|
|
|
return i.Action + "_" + i.Key
|
|
|
|
}
|
|
|
|
|
2019-08-18 05:21:27 -04:00
|
|
|
k := strconv.Itoa(i.Width) + "x" + strconv.Itoa(i.Height)
|
|
|
|
if i.Action != "" {
|
|
|
|
k += "_" + i.Action
|
|
|
|
}
|
|
|
|
if i.Quality > 0 {
|
|
|
|
k += "_q" + strconv.Itoa(i.Quality)
|
|
|
|
}
|
|
|
|
if i.Rotate != 0 {
|
|
|
|
k += "_r" + strconv.Itoa(i.Rotate)
|
|
|
|
}
|
2019-10-20 04:39:00 -04:00
|
|
|
if i.BgColorStr != "" {
|
|
|
|
k += "_bg" + i.BgColorStr
|
|
|
|
}
|
|
|
|
|
2019-08-18 05:21:27 -04:00
|
|
|
anchor := i.AnchorStr
|
2019-08-26 13:12:41 -04:00
|
|
|
if anchor == smartCropIdentifier {
|
2019-08-18 05:21:27 -04:00
|
|
|
anchor = anchor + strconv.Itoa(smartCropVersionNumber)
|
|
|
|
}
|
|
|
|
|
|
|
|
k += "_" + i.FilterStr
|
|
|
|
|
|
|
|
if strings.EqualFold(i.Action, "fill") {
|
|
|
|
k += "_" + anchor
|
|
|
|
}
|
|
|
|
|
|
|
|
if v, ok := imageFormatsVersions[format]; ok {
|
|
|
|
k += "_" + strconv.Itoa(v)
|
|
|
|
}
|
|
|
|
|
|
|
|
if mainImageVersionNumber > 0 {
|
|
|
|
k += "_" + strconv.Itoa(mainImageVersionNumber)
|
|
|
|
}
|
|
|
|
|
|
|
|
return k
|
|
|
|
}
|
|
|
|
|
2019-10-20 04:39:00 -04:00
|
|
|
type ImagingConfig struct {
|
|
|
|
BgColor color.Color
|
|
|
|
|
|
|
|
// Config as provided by the user.
|
|
|
|
Cfg Imaging
|
|
|
|
}
|
|
|
|
|
2019-08-18 05:21:27 -04:00
|
|
|
// Imaging contains default image processing configuration. This will be fetched
|
|
|
|
// from site (or language) config.
|
|
|
|
type Imaging struct {
|
|
|
|
// Default image quality setting (1-100). Only used for JPEG images.
|
|
|
|
Quality int
|
|
|
|
|
2019-08-26 13:12:41 -04:00
|
|
|
// Resample filter to use in resize operations..
|
2019-08-18 05:21:27 -04:00
|
|
|
ResampleFilter string
|
|
|
|
|
2019-08-26 13:12:41 -04:00
|
|
|
// The anchor to use in Fill. Default is "smart", i.e. Smart Crop.
|
2019-08-18 05:21:27 -04:00
|
|
|
Anchor string
|
2019-08-29 04:18:51 -04:00
|
|
|
|
2019-10-20 04:39:00 -04:00
|
|
|
// Default color used in fill operations (e.g. "fff" for white).
|
|
|
|
BgColor string
|
|
|
|
|
2019-08-29 04:18:51 -04:00
|
|
|
Exif ExifConfig
|
|
|
|
}
|
|
|
|
|
|
|
|
type ExifConfig struct {
|
|
|
|
|
|
|
|
// Regexp matching the Exif fields you want from the (massive) set of Exif info
|
|
|
|
// available. As we cache this info to disk, this is for performance and
|
|
|
|
// disk space reasons more than anything.
|
|
|
|
// If you want it all, put ".*" in this config setting.
|
|
|
|
// Note that if neither this or ExcludeFields is set, Hugo will return a small
|
|
|
|
// default set.
|
|
|
|
IncludeFields string
|
|
|
|
|
|
|
|
// Regexp matching the Exif fields you want to exclude. This may be easier to use
|
|
|
|
// than IncludeFields above, depending on what you want.
|
|
|
|
ExcludeFields string
|
|
|
|
|
|
|
|
// Hugo extracts the "photo taken" date/time into .Date by default.
|
|
|
|
// Set this to true to turn it off.
|
|
|
|
DisableDate bool
|
|
|
|
|
|
|
|
// Hugo extracts the "photo taken where" (GPS latitude and longitude) into
|
|
|
|
// .Long and .Lat. Set this to true to turn it off.
|
|
|
|
DisableLatLong bool
|
2019-08-18 05:21:27 -04:00
|
|
|
}
|