Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/gohugoio/hugo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2019-01-02 13:58:32 +0300
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2019-01-02 16:25:37 +0300
commitce8a09a4c0661dece931ab1173e4f09e8e04aa38 (patch)
tree870372211e342312d0ab3034ee03285c6f698f9d /resource/image.go
parent669ada436787311cc5d02dae5b88e60a09adda58 (diff)
resources: Move resource interfaces into its own package
Diffstat (limited to 'resource/image.go')
-rw-r--r--resource/image.go601
1 files changed, 0 insertions, 601 deletions
diff --git a/resource/image.go b/resource/image.go
deleted file mode 100644
index 6dea4cbb8..000000000
--- a/resource/image.go
+++ /dev/null
@@ -1,601 +0,0 @@
-// Copyright 2017-present 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 resource
-
-import (
- "errors"
- "fmt"
- "image"
- "image/color"
- "image/draw"
- "image/jpeg"
- "io"
- "io/ioutil"
- "os"
- "strconv"
- "strings"
- "sync"
-
- _errors "github.com/pkg/errors"
-
- "github.com/disintegration/imaging"
- "github.com/gohugoio/hugo/common/hugio"
- "github.com/gohugoio/hugo/helpers"
- "github.com/mitchellh/mapstructure"
-
- // Blind import for image.Decode
- _ "image/gif"
- _ "image/png"
-
- // Blind import for image.Decode
- _ "golang.org/x/image/webp"
-)
-
-var (
- _ Resource = (*Image)(nil)
- _ Source = (*Image)(nil)
- _ Cloner = (*Image)(nil)
-)
-
-// 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 used. See https://github.com/disintegration/imaging
- ResampleFilter string
-
- // The anchor used in Fill. Default is "smart", i.e. Smart Crop.
- Anchor string
-}
-
-const (
- defaultJPEGQuality = 75
- defaultResampleFilter = "box"
-)
-
-var (
- imageFormats = map[string]imaging.Format{
- ".jpg": imaging.JPEG,
- ".jpeg": imaging.JPEG,
- ".png": imaging.PNG,
- ".tif": imaging.TIFF,
- ".tiff": imaging.TIFF,
- ".bmp": imaging.BMP,
- ".gif": imaging.GIF,
- }
-
- // Add or increment if changes to an image format's processing requires
- // re-generation.
- imageFormatsVersions = map[imaging.Format]int{
- imaging.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]imaging.Anchor{
- strings.ToLower("Center"): imaging.Center,
- strings.ToLower("TopLeft"): imaging.TopLeft,
- strings.ToLower("Top"): imaging.Top,
- strings.ToLower("TopRight"): imaging.TopRight,
- strings.ToLower("Left"): imaging.Left,
- strings.ToLower("Right"): imaging.Right,
- strings.ToLower("BottomLeft"): imaging.BottomLeft,
- strings.ToLower("Bottom"): imaging.Bottom,
- strings.ToLower("BottomRight"): imaging.BottomRight,
-}
-
-var imageFilters = map[string]imaging.ResampleFilter{
- strings.ToLower("NearestNeighbor"): imaging.NearestNeighbor,
- strings.ToLower("Box"): imaging.Box,
- strings.ToLower("Linear"): imaging.Linear,
- strings.ToLower("Hermite"): imaging.Hermite,
- strings.ToLower("MitchellNetravali"): imaging.MitchellNetravali,
- strings.ToLower("CatmullRom"): imaging.CatmullRom,
- strings.ToLower("BSpline"): imaging.BSpline,
- strings.ToLower("Gaussian"): imaging.Gaussian,
- strings.ToLower("Lanczos"): imaging.Lanczos,
- strings.ToLower("Hann"): imaging.Hann,
- strings.ToLower("Hamming"): imaging.Hamming,
- strings.ToLower("Blackman"): imaging.Blackman,
- strings.ToLower("Bartlett"): imaging.Bartlett,
- strings.ToLower("Welch"): imaging.Welch,
- strings.ToLower("Cosine"): imaging.Cosine,
-}
-
-// Image represents an image resource.
-type Image struct {
- config image.Config
- configInit sync.Once
- configLoaded bool
-
- copyToDestinationInit sync.Once
-
- imaging *Imaging
-
- format imaging.Format
-
- *genericResource
-}
-
-// Width returns i's width.
-func (i *Image) Width() int {
- i.initConfig()
- return i.config.Width
-}
-
-// Height returns i's height.
-func (i *Image) Height() int {
- i.initConfig()
- return i.config.Height
-}
-
-// WithNewBase implements the Cloner interface.
-func (i *Image) WithNewBase(base string) Resource {
- return &Image{
- imaging: i.imaging,
- format: i.format,
- genericResource: i.genericResource.WithNewBase(base).(*genericResource)}
-}
-
-// Resize resizes the image to the specified width and height using the specified resampling
-// filter and returns the transformed image. If one of width or height is 0, the image aspect
-// ratio is preserved.
-func (i *Image) Resize(spec string) (*Image, error) {
- return i.doWithImageConfig("resize", spec, func(src image.Image, conf imageConfig) (image.Image, error) {
- return imaging.Resize(src, conf.Width, conf.Height, conf.Filter), nil
- })
-}
-
-// Fit scales down the image using the specified resample filter to fit the specified
-// maximum width and height.
-func (i *Image) Fit(spec string) (*Image, error) {
- return i.doWithImageConfig("fit", spec, func(src image.Image, conf imageConfig) (image.Image, error) {
- return imaging.Fit(src, conf.Width, conf.Height, conf.Filter), nil
- })
-}
-
-// Fill scales the image to the smallest possible size that will cover the specified dimensions,
-// crops the resized image to the specified dimensions using the given anchor point.
-// Space delimited config: 200x300 TopLeft
-func (i *Image) Fill(spec string) (*Image, error) {
- return i.doWithImageConfig("fill", spec, func(src image.Image, conf imageConfig) (image.Image, error) {
- if conf.AnchorStr == smartCropIdentifier {
- return smartCrop(src, conf.Width, conf.Height, conf.Anchor, conf.Filter)
- }
- return imaging.Fill(src, conf.Width, conf.Height, conf.Anchor, conf.Filter), nil
- })
-}
-
-// Holds configuration to create a new image from an existing one, resize etc.
-type imageConfig struct {
- Action 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 imaging.ResampleFilter
- FilterStr string
-
- Anchor imaging.Anchor
- AnchorStr string
-}
-
-func (i *Image) isJPEG() bool {
- name := strings.ToLower(i.relTargetDirFile.file)
- return strings.HasSuffix(name, ".jpg") || strings.HasSuffix(name, ".jpeg")
-}
-
-// Serialize image processing. The imaging library spins up its own set of Go routines,
-// so there is not much to gain from adding more load to the mix. That
-// can even have negative effect in low resource scenarios.
-// Note that this only effects the non-cached scenario. Once the processed
-// image is written to disk, everything is fast, fast fast.
-const imageProcWorkers = 1
-
-var imageProcSem = make(chan bool, imageProcWorkers)
-
-func (i *Image) doWithImageConfig(action, spec string, f func(src image.Image, conf imageConfig) (image.Image, error)) (*Image, error) {
- conf, err := parseImageConfig(spec)
- if err != nil {
- return nil, err
- }
- conf.Action = action
-
- if conf.Quality <= 0 && i.isJPEG() {
- // We need a quality setting for all JPEGs
- conf.Quality = i.imaging.Quality
- }
-
- if conf.FilterStr == "" {
- conf.FilterStr = i.imaging.ResampleFilter
- conf.Filter = imageFilters[conf.FilterStr]
- }
-
- if conf.AnchorStr == "" {
- conf.AnchorStr = i.imaging.Anchor
- if !strings.EqualFold(conf.AnchorStr, smartCropIdentifier) {
- conf.Anchor = anchorPositions[conf.AnchorStr]
- }
- }
-
- return i.spec.imageCache.getOrCreate(i, conf, func() (*Image, image.Image, error) {
- imageProcSem <- true
- defer func() {
- <-imageProcSem
- }()
-
- ci := i.clone()
-
- errOp := action
- errPath := i.sourceFilename
-
- ci.setBasePath(conf)
-
- src, err := i.decodeSource()
- if err != nil {
- return nil, nil, &os.PathError{Op: errOp, Path: errPath, Err: err}
- }
-
- if conf.Rotate != 0 {
- // Rotate it before any scaling to get the dimensions correct.
- src = imaging.Rotate(src, float64(conf.Rotate), color.Transparent)
- }
-
- converted, err := f(src, conf)
- if err != nil {
- return ci, nil, &os.PathError{Op: errOp, Path: errPath, Err: err}
- }
-
- if i.format == imaging.PNG {
- // Apply the colour palette from the source
- if paletted, ok := src.(*image.Paletted); ok {
- tmp := image.NewPaletted(converted.Bounds(), paletted.Palette)
- draw.FloydSteinberg.Draw(tmp, tmp.Bounds(), converted, converted.Bounds().Min)
- converted = tmp
- }
- }
-
- b := converted.Bounds()
- ci.config = image.Config{Width: b.Max.X, Height: b.Max.Y}
- ci.configLoaded = true
-
- return ci, converted, nil
- })
-
-}
-
-func (i imageConfig) key(format imaging.Format) string {
- 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
-}
-
-func newImageConfig(width, height, quality, rotate int, filter, anchor string) imageConfig {
- var c imageConfig
-
- c.Width = width
- c.Height = height
- c.Quality = quality
- c.Rotate = rotate
-
- if filter != "" {
- filter = strings.ToLower(filter)
- if v, ok := imageFilters[filter]; ok {
- c.Filter = v
- c.FilterStr = filter
- }
- }
-
- if anchor != "" {
- anchor = strings.ToLower(anchor)
- if v, ok := anchorPositions[anchor]; ok {
- c.Anchor = v
- c.AnchorStr = anchor
- }
- }
-
- return c
-}
-
-func parseImageConfig(config string) (imageConfig, error) {
- var (
- c imageConfig
- err error
- )
-
- 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")
- }
-
- }
- }
-
- if c.Width == 0 && c.Height == 0 {
- return c, errors.New("must provide Width or Height")
- }
-
- return c, nil
-}
-
-func (i *Image) initConfig() error {
- var err error
- i.configInit.Do(func() {
- if i.configLoaded {
- return
- }
-
- var (
- f hugio.ReadSeekCloser
- config image.Config
- )
-
- f, err = i.ReadSeekCloser()
- if err != nil {
- return
- }
- defer f.Close()
-
- config, _, err = image.DecodeConfig(f)
- if err != nil {
- return
- }
- i.config = config
- })
-
- if err != nil {
- return _errors.Wrap(err, "failed to load image config")
- }
-
- return nil
-}
-
-func (i *Image) decodeSource() (image.Image, error) {
- f, err := i.ReadSeekCloser()
- if err != nil {
- return nil, _errors.Wrap(err, "failed to open image for decode")
- }
- defer f.Close()
- img, _, err := image.Decode(f)
- return img, err
-}
-
-func (i *Image) openDestinationsForWriting() (io.WriteCloser, error) {
- targetFilenames := i.targetFilenames()
- var changedFilenames []string
-
- // Fast path:
- // This is a processed version of the original.
- // If it exists on destination with the same filename and file size, it is
- // the same file, so no need to transfer it again.
- for _, targetFilename := range targetFilenames {
- if fi, err := i.spec.BaseFs.PublishFs.Stat(targetFilename); err == nil && fi.Size() == i.osFileInfo.Size() {
- continue
- }
- changedFilenames = append(changedFilenames, targetFilename)
- }
-
- if len(changedFilenames) == 0 {
- return struct {
- io.Writer
- io.Closer
- }{
- ioutil.Discard,
- ioutil.NopCloser(nil),
- }, nil
-
- }
-
- return helpers.OpenFilesForWriting(i.spec.BaseFs.PublishFs, changedFilenames...)
-
-}
-
-func (i *Image) encodeTo(conf imageConfig, img image.Image, w io.Writer) error {
- switch i.format {
- case imaging.JPEG:
-
- var rgba *image.RGBA
- quality := conf.Quality
-
- if nrgba, ok := img.(*image.NRGBA); ok {
- if nrgba.Opaque() {
- rgba = &image.RGBA{
- Pix: nrgba.Pix,
- Stride: nrgba.Stride,
- Rect: nrgba.Rect,
- }
- }
- }
- if rgba != nil {
- return jpeg.Encode(w, rgba, &jpeg.Options{Quality: quality})
- }
- return jpeg.Encode(w, img, &jpeg.Options{Quality: quality})
- default:
- return imaging.Encode(w, img, i.format)
- }
-}
-
-func (i *Image) clone() *Image {
- g := *i.genericResource
- g.resourceContent = &resourceContent{}
-
- return &Image{
- imaging: i.imaging,
- format: i.format,
- genericResource: &g}
-}
-
-func (i *Image) setBasePath(conf imageConfig) {
- i.relTargetDirFile = i.relTargetPathFromConfig(conf)
-}
-
-func (i *Image) relTargetPathFromConfig(conf imageConfig) dirFile {
- p1, p2 := helpers.FileAndExt(i.relTargetDirFile.file)
-
- idStr := fmt.Sprintf("_hu%s_%d", i.hash, i.osFileInfo.Size())
-
- // Do not change for no good reason.
- const md5Threshold = 100
-
- key := conf.key(i.format)
-
- // It is useful to have the key in clear text, but when nesting transforms, it
- // can easily be too long to read, and maybe even too long
- // for the different OSes to handle.
- if len(p1)+len(idStr)+len(p2) > md5Threshold {
- key = helpers.MD5String(p1 + key + p2)
- huIdx := strings.Index(p1, "_hu")
- if huIdx != -1 {
- p1 = p1[:huIdx]
- } else {
- // This started out as a very long file name. Making it even longer
- // could melt ice in the Arctic.
- p1 = ""
- }
- } else if strings.Contains(p1, idStr) {
- // On scaling an already scaled image, we get the file info from the original.
- // Repeating the same info in the filename makes it stuttery for no good reason.
- idStr = ""
- }
-
- return dirFile{
- dir: i.relTargetDirFile.dir,
- file: fmt.Sprintf("%s%s_%s%s", p1, idStr, key, p2),
- }
-
-}
-
-func decodeImaging(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
- }
-
- return i, nil
-}