summaryrefslogtreecommitdiffstats
path: root/resources
diff options
context:
space:
mode:
Diffstat (limited to 'resources')
-rw-r--r--resources/image.go506
-rw-r--r--resources/image_cache.go63
-rw-r--r--resources/image_test.go189
-rw-r--r--resources/images/config.go276
-rw-r--r--resources/images/config_test.go125
-rw-r--r--resources/images/image.go170
-rw-r--r--resources/images/smartcrop.go (renamed from resources/smartcrop.go)16
-rw-r--r--resources/internal/key.go61
-rw-r--r--resources/internal/key_test.go36
-rw-r--r--resources/resource.go827
-rw-r--r--resources/resource/resourcetypes.go20
-rw-r--r--resources/resource_cache.go2
-rw-r--r--resources/resource_metadata.go20
-rw-r--r--resources/resource_metadata_test.go2
-rw-r--r--resources/resource_spec.go304
-rw-r--r--resources/resource_test.go25
-rw-r--r--resources/resource_transformers/htesting/testhelpers.go80
-rw-r--r--resources/resource_transformers/integrity/integrity.go25
-rw-r--r--resources/resource_transformers/integrity/integrity_test.go24
-rw-r--r--resources/resource_transformers/minifier/minify.go18
-rw-r--r--resources/resource_transformers/minifier/minify_test.go43
-rw-r--r--resources/resource_transformers/postcss/postcss.go12
-rw-r--r--resources/resource_transformers/templates/execute_as_template.go22
-rw-r--r--resources/resource_transformers/tocss/scss/client.go12
-rw-r--r--resources/testhelpers_test.go44
-rw-r--r--resources/transform.go598
-rw-r--r--resources/transform_test.go428
27 files changed, 2492 insertions, 1456 deletions
diff --git a/resources/image.go b/resources/image.go
index f1aae2996..e1a816942 100644
--- a/resources/image.go
+++ b/resources/image.go
@@ -14,198 +14,98 @@
package resources
import (
- "errors"
"fmt"
"image"
"image/color"
"image/draw"
- "image/jpeg"
- "io"
+ _ "image/gif"
+ _ "image/png"
"os"
- "strconv"
"strings"
- "sync"
"github.com/gohugoio/hugo/resources/resource"
_errors "github.com/pkg/errors"
"github.com/disintegration/imaging"
- "github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/helpers"
- "github.com/mitchellh/mapstructure"
+ "github.com/gohugoio/hugo/resources/images"
// Blind import for image.Decode
- _ "image/gif"
- _ "image/png"
// Blind import for image.Decode
_ "golang.org/x/image/webp"
)
var (
- _ resource.Resource = (*Image)(nil)
- _ resource.Source = (*Image)(nil)
- _ resource.Cloner = (*Image)(nil)
+ _ resource.Image = (*imageResource)(nil)
+ _ resource.Source = (*imageResource)(nil)
+ _ resource.Cloner = (*imageResource)(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
+// ImageResource represents an image resource.
+type imageResource struct {
+ *images.Image
- // The anchor used in Fill. Default is "smart", i.e. Smart Crop.
- Anchor string
+ baseResource
}
-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,
+func (i *imageResource) Clone() resource.Resource {
+ gr := i.baseResource.Clone().(baseResource)
+ return &imageResource{
+ Image: i.WithSpec(gr),
+ baseResource: gr,
}
-
- // 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
-
- imaging *Imaging
-
- format imaging.Format
-
- *genericResource
-}
+func (i *imageResource) cloneWithUpdates(u *transformationUpdate) (baseResource, error) {
+ base, err := i.baseResource.cloneWithUpdates(u)
+ if err != nil {
+ return nil, err
+ }
-// Width returns i's width.
-func (i *Image) Width() int {
- i.initConfig()
- return i.config.Width
-}
+ var img *images.Image
-// Height returns i's height.
-func (i *Image) Height() int {
- i.initConfig()
- return i.config.Height
-}
+ if u.isContenChanged() {
+ img = i.WithSpec(base)
+ } else {
+ img = i.Image
+ }
-// WithNewBase implements the Cloner interface.
-func (i *Image) WithNewBase(base string) resource.Resource {
- return &Image{
- imaging: i.imaging,
- format: i.format,
- genericResource: i.genericResource.WithNewBase(base).(*genericResource)}
+ return &imageResource{
+ Image: img,
+ baseResource: base,
+ }, nil
}
// 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
+func (i *imageResource) Resize(spec string) (resource.Image, error) {
+ return i.doWithImageConfig("resize", spec, func(src image.Image, conf images.ImageConfig) (image.Image, error) {
+ return i.Proc.Resize(src, conf)
})
}
// 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
+func (i *imageResource) Fit(spec string) (resource.Image, error) {
+ return i.doWithImageConfig("fit", spec, func(src image.Image, conf images.ImageConfig) (image.Image, error) {
+ return i.Proc.Fit(src, conf)
})
}
// 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
+func (i *imageResource) Fill(spec string) (resource.Image, error) {
+ return i.doWithImageConfig("fill", spec, func(src image.Image, conf images.ImageConfig) (image.Image, error) {
+ return i.Proc.Fill(src, conf)
})
}
-// 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)
+func (i *imageResource) isJPEG() bool {
+ name := strings.ToLower(i.getResourcePaths().relTargetDirFile.file)
return strings.HasSuffix(name, ".jpg") || strings.HasSuffix(name, ".jpeg")
}
@@ -218,42 +118,20 @@ 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)
+func (i *imageResource) doWithImageConfig(action, spec string, f func(src image.Image, conf images.ImageConfig) (image.Image, error)) (resource.Image, error) {
+ conf, err := i.decodeImageConfig(action, 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) {
+ return i.getSpec().imageCache.getOrCreate(i, conf, func() (*imageResource, image.Image, error) {
imageProcSem <- true
defer func() {
<-imageProcSem
}()
- ci := i.clone()
-
errOp := action
- errPath := i.sourceFilename
-
- ci.setBasePath(conf)
+ errPath := i.getSourceFilename()
src, err := i.decodeSource()
if err != nil {
@@ -267,10 +145,10 @@ func (i *Image) doWithImageConfig(action, spec string, f func(src image.Image, c
converted, err := f(src, conf)
if err != nil {
- return ci, nil, &os.PathError{Op: errOp, Path: errPath, Err: err}
+ return nil, nil, &os.PathError{Op: errOp, Path: errPath, Err: err}
}
- if i.format == imaging.PNG {
+ 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)
@@ -279,177 +157,30 @@ func (i *Image) doWithImageConfig(action, spec string, f func(src image.Image, c
}
}
- b := converted.Bounds()
- ci.config = image.Config{Width: b.Max.X, Height: b.Max.Y}
- ci.configLoaded = true
+ ci := i.clone(converted)
+ ci.setBasePath(conf)
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")
+func (i *imageResource) decodeImageConfig(action, spec string) (images.ImageConfig, error) {
+ conf, err := images.DecodeImageConfig(action, spec, i.Proc.Cfg)
+ if err != nil {
+ return conf, err
}
- 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
- })
+ iconf := i.Proc.Cfg
- if err != nil {
- return _errors.Wrap(err, "failed to load image config")
+ if conf.Quality <= 0 && i.isJPEG() {
+ // We need a quality setting for all JPEGs
+ conf.Quality = iconf.Quality
}
- return nil
+ return conf, nil
}
-func (i *Image) decodeSource() (image.Image, error) {
+func (i *imageResource) decodeSource() (image.Image, error) {
f, err := i.ReadSeekCloser()
if err != nil {
return nil, _errors.Wrap(err, "failed to open image for decode")
@@ -459,80 +190,39 @@ func (i *Image) decodeSource() (image.Image, error) {
return img, err
}
-// returns an opened file or nil if nothing to write.
-func (i *Image) openDestinationsForWriting() (io.WriteCloser, error) {
- targetFilenames := i.targetFilenames()
- var changedFilenames []string
-
- // Fast path:
- // This is a processed version of the original;
- // check if it already existis at the destination.
- for _, targetFilename := range targetFilenames {
- if _, err := i.spec.BaseFs.PublishFs.Stat(targetFilename); err == nil {
- continue
- }
- changedFilenames = append(changedFilenames, targetFilename)
- }
-
- if len(changedFilenames) == 0 {
- return 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
+func (i *imageResource) clone(img image.Image) *imageResource {
+ spec := i.baseResource.Clone().(baseResource)
- 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)
+ var image *images.Image
+ if img != nil {
+ image = i.WithImage(img)
+ } else {
+ image = i.WithSpec(spec)
}
-}
-func (i *Image) clone() *Image {
- g := *i.genericResource
- g.resourceContent = &resourceContent{}
- if g.publishOnce != nil {
- g.publishOnce = &publishOnce{logger: g.publishOnce.logger}
+ return &imageResource{
+ Image: image,
+ baseResource: spec,
}
-
- return &Image{
- imaging: i.imaging,
- format: i.format,
- genericResource: &g}
}
-func (i *Image) setBasePath(conf imageConfig) {
- i.relTargetDirFile = i.relTargetPathFromConfig(conf)
+func (i *imageResource) setBasePath(conf images.ImageConfig) {
+ i.getResourcePaths().relTargetDirFile = i.relTargetPathFromConfig(conf)
}
-func (i *Image) relTargetPathFromConfig(conf imageConfig) dirFile {
- p1, p2 := helpers.FileAndExt(i.relTargetDirFile.file)
+func (i *imageResource) relTargetPathFromConfig(conf images.ImageConfig) dirFile {
+ p1, p2 := helpers.FileAndExt(i.getResourcePaths().relTargetDirFile.file)
+ if conf.Action == "trace" {
+ p2 = ".svg"
+ }
- idStr := fmt.Sprintf("_hu%s_%d", i.hash, i.osFileInfo.Size())
+ h, _ := i.hash()
+ idStr := fmt.Sprintf("_hu%s_%d", h, i.size())
// Do not change for no good reason.
const md5Threshold = 100
- key := conf.key(i.format)
+ 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
@@ -554,43 +244,7 @@ func (i *Image) relTargetPathFromConfig(conf imageConfig) dirFile {
}
return dirFile{
- dir: i.relTargetDirFile.dir,
+ dir: i.getResourcePaths().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
}
diff --git a/resources/image_cache.go b/resources/image_cache.go
index 3324e442e..3a9e3c2c5 100644
--- a/resources/image_cache.go
+++ b/resources/image_cache.go
@@ -20,7 +20,7 @@ import (
"strings"
"sync"
- "github.com/gohugoio/hugo/common/hugio"
+ "github.com/gohugoio/hugo/resources/images"
"github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/helpers"
@@ -32,7 +32,7 @@ type imageCache struct {
fileCache *filecache.Cache
mu sync.RWMutex
- store map[string]*Image
+ store map[string]*resourceAdapter
}
func (c *imageCache) isInCache(key string) bool {
@@ -66,33 +66,34 @@ func (c *imageCache) normalizeKey(key string) string {
func (c *imageCache) clear() {
c.mu.Lock()
defer c.mu.Unlock()
- c.store = make(map[string]*Image)
+ c.store = make(map[string]*resourceAdapter)
}
func (c *imageCache) getOrCreate(
- parent *Image, conf imageConfig, createImage func() (*Image, image.Image, error)) (*Image, error) {
-
+ parent *imageResource, conf images.ImageConfig,
+ createImage func() (*imageResource, image.Image, error)) (*resourceAdapter, error) {
relTarget := parent.relTargetPathFromConfig(conf)
key := parent.relTargetPathForRel(relTarget.path(), false, false, false)
// First check the in-memory store, then the disk.
c.mu.RLock()
- img, found := c.store[key]
+ cachedImage, found := c.store[key]
c.mu.RUnlock()
if found {
- return img, nil
+ return cachedImage, nil
}
+ var img *imageResource
+
// These funcs are protected by a named lock.
// read clones the parent to its new name and copies
// the content to the destinations.
read := func(info filecache.ItemInfo, r io.Reader) error {
- img = parent.clone()
- img.relTargetDirFile.file = relTarget.file
- img.sourceFilename = info.Name
- // Make sure it's always loaded by sourceFilename.
- img.openReadSeekerCloser = nil
+ img = parent.clone(nil)
+ rp := img.getResourcePaths()
+ rp.relTargetDirFile.file = relTarget.file
+ img.setSourceFilename(info.Name)
w, err := img.openDestinationsForWriting()
if err != nil {
@@ -109,29 +110,20 @@ func (c *imageCache) getOrCreate(
return err
}
- // create creates the image and encodes it to w (cache) and to its destinations.
+ // create creates the image and encodes it to the cache (w).
create := func(info filecache.ItemInfo, w io.WriteCloser) (err error) {
+ defer w.Close()
+
var conv image.Image
img, conv, err = createImage()
if err != nil {
- w.Close()
return
}
- img.relTargetDirFile.file = relTarget.file
- img.sourceFilename = info.Name
+ rp := img.getResourcePaths()
+ rp.relTargetDirFile.file = relTarget.file
+ img.setSourceFilename(info.Name)
- destinations, err := img.openDestinationsForWriting()
- if err != nil {
- w.Close()
- return err
- }
-
- if destinations != nil {
- w = hugio.NewMultiWriteCloser(w, destinations)
- }
- defer w.Close