mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-07 20:30:36 -05:00
e5856e61d8
The image format is defined as the image extension of the known formats, excluding the dot. All of 'img.Resize "600x jpeg"', 'img.Resize "600x jpg"', and 'img.Resize "600x png"' are valid format definitions. If the target format is defined in the operation definition string, then the converted image will be stored in this format. Permalinks and media type are updated correspondingly. Unknown image extensions in the operation definition have not effect. See #6298
316 lines
8.5 KiB
Go
316 lines
8.5 KiB
Go
// 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"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/disintegration/gift"
|
|
|
|
"github.com/mitchellh/mapstructure"
|
|
)
|
|
|
|
const (
|
|
defaultJPEGQuality = 75
|
|
defaultResampleFilter = "box"
|
|
)
|
|
|
|
var (
|
|
imageFormats = map[string]Format{
|
|
".jpg": JPEG,
|
|
".jpeg": JPEG,
|
|
".png": PNG,
|
|
".tif": TIFF,
|
|
".tiff": TIFF,
|
|
".bmp": BMP,
|
|
".gif": GIF,
|
|
}
|
|
|
|
// Add or increment if changes to an image format's processing requires
|
|
// re-generation.
|
|
imageFormatsVersions = map[Format]int{
|
|
PNG: 2, // Floyd Steinberg dithering
|
|
}
|
|
|
|
// Increment to mark all processed images as stale. Only use when absolutely needed.
|
|
// See the finer grained smartCropVersionNumber and imageFormatsVersions.
|
|
mainImageVersionNumber = 0
|
|
)
|
|
|
|
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,
|
|
}
|
|
|
|
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,
|
|
}
|
|
|
|
func ImageFormatFromExt(ext string) (Format, bool) {
|
|
f, found := imageFormats[ext]
|
|
return f, found
|
|
}
|
|
|
|
func DecodeConfig(m map[string]interface{}) (Imaging, error) {
|
|
var i Imaging
|
|
if err := mapstructure.WeakDecode(m, &i); err != nil {
|
|
return i, err
|
|
}
|
|
|
|
if i.Quality == 0 {
|
|
i.Quality = defaultJPEGQuality
|
|
} else if i.Quality < 0 || i.Quality > 100 {
|
|
return i, errors.New("JPEG quality must be a number between 1 and 100")
|
|
}
|
|
|
|
if i.Anchor == "" || strings.EqualFold(i.Anchor, smartCropIdentifier) {
|
|
i.Anchor = smartCropIdentifier
|
|
} else {
|
|
i.Anchor = strings.ToLower(i.Anchor)
|
|
if _, found := anchorPositions[i.Anchor]; !found {
|
|
return i, errors.New("invalid anchor value in imaging config")
|
|
}
|
|
}
|
|
|
|
if i.ResampleFilter == "" {
|
|
i.ResampleFilter = defaultResampleFilter
|
|
} else {
|
|
filter := strings.ToLower(i.ResampleFilter)
|
|
_, found := imageFilters[filter]
|
|
if !found {
|
|
return i, fmt.Errorf("%q is not a valid resample filter", filter)
|
|
}
|
|
i.ResampleFilter = filter
|
|
}
|
|
|
|
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"
|
|
}
|
|
|
|
return i, nil
|
|
}
|
|
|
|
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)
|
|
|
|
if part == smartCropIdentifier {
|
|
c.AnchorStr = smartCropIdentifier
|
|
} 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
|
|
} 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")
|
|
}
|
|
} else if f, ok := ImageFormatFromExt("." + part); ok {
|
|
c.TargetFormat = f
|
|
}
|
|
}
|
|
|
|
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
|
|
if !strings.EqualFold(c.AnchorStr, smartCropIdentifier) {
|
|
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 {
|
|
// This defines the output format of the output image. It defaults to the source format
|
|
TargetFormat Format
|
|
|
|
Action string
|
|
|
|
// If set, this will be used as the key in filenames etc.
|
|
Key string
|
|
|
|
// 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
|
|
|
|
Width int
|
|
Height int
|
|
|
|
Filter gift.Resampling
|
|
FilterStr string
|
|
|
|
Anchor gift.Anchor
|
|
AnchorStr string
|
|
}
|
|
|
|
func (i ImageConfig) GetKey(format Format) string {
|
|
if i.Key != "" {
|
|
return i.Action + "_" + i.Key
|
|
}
|
|
|
|
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)
|
|
}
|
|
anchor := i.AnchorStr
|
|
if anchor == smartCropIdentifier {
|
|
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
|
|
}
|
|
|
|
// 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
|
|
|
|
// Resample filter to use in resize operations..
|
|
ResampleFilter string
|
|
|
|
// The anchor to use in Fill. Default is "smart", i.e. Smart Crop.
|
|
Anchor string
|
|
|
|
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
|
|
}
|