rolesSorted := cfg.Configs.Base.Roles.Config.Sorted
versionsSorted := cfg.Configs.Base.Versions.Config.Sorted
+ var (
+ poolSizeKatex = 2
+ poolSizeWebP = 1
+ )
+ if n := config.GetNumWorkerMultiplier(); n > 1 {
+ poolSizeKatex = min(n, 8)
+ poolSizeWebP = max(2, n/2)
+ }
+
var logger loggers.Logger
if cfg.TestLogger != nil {
logger = cfg.TestLogger
warpc.Options{
CompilationCacheDir: compilationCacheDir,
- // Katex is relatively slow.
- PoolSize: 8,
+ PoolSize: poolSizeKatex,
Infof: logger.InfoCommand("katex").Logf,
Warnf: logger.WarnCommand("katex").Logf,
},
// WebP options.
warpc.Options{
CompilationCacheDir: compilationCacheDir,
- PoolSize: 1,
+ PoolSize: poolSizeWebP,
Memory: 384, // 384 MiB (4096 MiB Max)
Infof: logger.InfoCommand("webp").Logf,
Warnf: logger.WarnCommand("webp").Logf,
int loopCount;
int frameCount;
int *frameDurations;
+ bool hasAlpha;
} InputParams;
int preset; // preset to use; resolved from hint.
bool useSharpYuv; // use sharp YUV for better quality.
+ int method; // quality/speed trade-off (0=fast, 6=slower-better). Default is 2.
} InputOptions;
}
config->use_sharp_yuv = opts.useSharpYuv ? 1 : 0;
+ config->method = opts.method;
return 1;
}
msg.data.options.hint[sizeof(msg.data.options.hint) - 1] = '\0';
}
msg.data.options.useSharpYuv = json_object_get_number(options_object, "useSharpYuv") != 0;
+ msg.data.options.method = (int)json_object_get_number(options_object, "method");
+ if (msg.data.options.method < 0 || msg.data.options.method > 6)
+ {
+ msg.data.options.method = 2; // default
+ }
if (msg.data.options.quality < 0 || msg.data.options.quality > 100)
{
msg.data.options.quality = 75; // default
json_object_set_number(params_object, "width", msg->data.params.width);
json_object_set_number(params_object, "height", msg->data.params.height);
json_object_set_number(params_object, "stride", msg->data.params.stride);
+ json_object_set_boolean(params_object, "hasAlpha", msg->data.params.hasAlpha);
if (msg->data.params.frameDurations != NULL)
{
JSON_Value *durations_value = json_value_init_array();
output.data.params.stride = anim_info.canvas_width * 4;
output.data.params.frameCount = anim_info.frame_count;
output.data.params.loopCount = anim_info.loop_count;
+ output.data.params.hasAlpha = true; // Animated WebP always decoded as RGBA
int *frameDurations = malloc(sizeof(int) * anim_info.frame_count);
if (frameDurations == NULL)
}
else
{
- uint8_t *output_buffer = WebPDecodeRGBA(blob_data, (size_t)blob_size, &output.data.params.width, &output.data.params.height);
+ uint8_t *output_buffer;
+ int bytesPerPixel;
+
+ output.data.params.hasAlpha = config.input.has_alpha;
+
+ if (config.input.has_alpha)
+ {
+ output_buffer = WebPDecodeRGBA(blob_data, (size_t)blob_size, &output.data.params.width, &output.data.params.height);
+ bytesPerPixel = 4;
+ }
+ else
+ {
+ output_buffer = WebPDecodeRGB(blob_data, (size_t)blob_size, &output.data.params.width, &output.data.params.height);
+ bytesPerPixel = 3;
+ }
+
if (output_buffer == NULL)
{
strncpy(output.header.err, "Failed to decode WebP", sizeof(output.header.err) - 1);
goto cleanup;
}
- output.data.params.stride = output.data.params.width * 4;
+ output.data.params.stride = output.data.params.width * bytesPerPixel;
write_output_message(&output);
)
type CommonImageProcessingParams struct {
- Width int `json:"width,omitempty"`
- Height int `json:"height,omitempty"`
- Stride int `json:"stride,omitempty"`
+ Width int `json:"width,omitempty"`
+ Height int `json:"height,omitempty"`
+ Stride int `json:"stride,omitempty"`
+ HasAlpha bool `json:"hasAlpha,omitempty"`
// For animated images.
FrameDurations []int `json:"frameDurations,omitempty"`
}
if len(out.Data.Params.FrameDurations) > 0 {
- // Animated WebP.
+ // Animated WebP (always RGBA).
img := &WEBP{
frameDurations: out.Data.Params.FrameDurations,
loopCount: out.Data.Params.LoopCount,
return img, nil
}
+ var pix []byte
+ var nrgbaStride int
+
+ if out.Data.Params.HasAlpha {
+ // RGBA data (4 bytes per pixel).
+ pix = destination.Bytes()
+ nrgbaStride = stride
+ } else {
+ // RGB data (3 bytes per pixel) - convert to NRGBA.
+ rgbData := destination.Bytes()
+ nrgbaStride = w * 4
+ pix = make([]byte, nrgbaStride*h)
+ for y := 0; y < h; y++ {
+ srcIdx := y * stride
+ dstIdx := y * nrgbaStride
+ for x := 0; x < w; x++ {
+ pix[dstIdx+0] = rgbData[srcIdx+0] // R
+ pix[dstIdx+1] = rgbData[srcIdx+1] // G
+ pix[dstIdx+2] = rgbData[srcIdx+2] // B
+ pix[dstIdx+3] = 0xFF // A (fully opaque)
+ srcIdx += 3
+ dstIdx += 4
+ }
+ }
+ }
+
img := &image.NRGBA{
- Pix: destination.Bytes(),
- Stride: stride,
+ Pix: pix,
+ Stride: nrgbaStride,
Rect: image.Rect(0, 0, w, h),
}
}
opts = maps.Clone(opts)
- opts["useSharpYuv"] = true // Use sharp (and slow) RGB->YUV conversion.
message := Message[WebpInput]{
Header: Header{
b.ImageHelper("public/anim_hu_edc2f24aaad2cee6.webp").AssertFormat("webp").AssertIsAnimated(true).AssertLoopCount(0).AssertFrameDurations(animFrameDurations)
b.ImageHelper("public/anim_hu_58eb49733894e7ce.gif").AssertFormat("gif").AssertIsAnimated(true).AssertLoopCount(0).AssertFrameDurations(animFrameDurations)
}
+
+func BenchmarkWebp(b *testing.B) {
+ files := `
+-- content/p1/sunrise.webp --
+sourcefilename: ../../resources/testdata/sunrise.webp
+-- content/p1/index.md --
+-- content/p2/sunrise.webp --
+sourcefilename: ../../resources/testdata/sunrise.webp
+-- content/p2/index.md --
+-- content/p3/sunrise.webp --
+sourcefilename: ../../resources/testdata/sunrise.webp
+-- content/p3/index.md --
+-- content/p4/sunrise.webp --
+sourcefilename: ../../resources/testdata/sunrise.webp
+-- content/p4/index.md --
+-- content/p5/sunrise.webp --
+sourcefilename: ../../resources/testdata/sunrise.webp
+-- content/p5/index.md --
+-- content/p6/sunrise.webp --
+sourcefilename: ../../resources/testdata/sunrise.webp
+-- content/p6/index.md --
+-- content/p7/sunrise.webp --
+sourcefilename: ../../resources/testdata/sunrise.webp
+-- content/p7/index.md --
+-- content/p8/sunrise.webp --
+sourcefilename: ../../resources/testdata/sunrise.webp
+-- content/p8/index.md --
+-- content/p9/sunrise.webp --
+sourcefilename: ../../resources/testdata/sunrise.webp
+-- content/p9/index.md --
+-- content/p10/sunrise.webp --
+sourcefilename: ../../resources/testdata/sunrise.webp
+-- content/p10/index.md --
+-- layouts/home.html --
+Home.
+-- layouts/page.html --
+{{ $image := .Resources.Get "sunrise.webp" }}
+{{ $resized := $image.Fit "400x300 webp" }}
+Resized RelPermalink: {{ $resized.RelPermalink }}|
+`
+
+ cfg := hugolib.IntegrationTestConfig{
+ T: b,
+ TxtarString: files,
+ }
+
+ for b.Loop() {
+ b.StopTimer()
+ builder := hugolib.NewIntegrationTestBuilder(cfg).Init()
+ b.StartTimer()
+ builder.Build()
+ }
+}
"github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/paths"
+ "github.com/gohugoio/hugo/config"
"github.com/gohugoio/gift"
return img, nil
}
-// 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 chan bool
-var imageProcSem = make(chan bool, imageProcWorkers)
+func init() {
+ imageProcWorkers := 1
+ if n := config.GetNumWorkerMultiplier(); n > 4 {
+ imageProcWorkers = 2
+ }
+ imageProcSem = make(chan bool, imageProcWorkers)
+}
func (i *imageResource) doWithImageConfig(conf images.ImageConfig, f func(src image.Image) (image.Image, error)) (images.ImageResource, error) {
img, err := i.getSpec().ImageCache.getOrCreate(i, conf, func() (*imageResource, image.Image, error) {
"compression": conf.Compression,
"quality": conf.Quality,
"hint": conf.Hint,
+ "useSharpYuv": conf.UseSharpYuv,
+ "method": conf.Method,
}
return d.webp.Encode(w, img, opts)
default:
}
const (
- defaultJPEGQuality = 75
- defaultResampleFilter = "box"
- defaultBgColor = "#ffffff"
- defaultHint = "photo"
- defaultCompression = "lossy"
+ defaultJPEGQuality = 75
+ defaultResampleFilter = "box"
+ defaultBgColor = "#ffffff"
+ defaultHint = "photo"
+ defaultCompression = "lossy"
+ defaultWebpUseSharpYuv = true
+ defaultWebpMethod = 4
)
var (
"hint": defaultHint,
"quality": defaultJPEGQuality,
"compression": defaultCompression,
+ "webp": map[string]any{
+ "useSharpYuv": defaultWebpUseSharpYuv,
+ "method": defaultWebpMethod,
+ },
}
defaultImageConfig *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]
// Merge in the defaults.
hmaps.MergeShallow(m, defaultImaging)
+ // Deep merge webp defaults.
+ if webp, ok := m["webp"].(map[string]any); ok {
+ hmaps.MergeShallow(webp, defaultImaging["webp"].(map[string]any))
+ }
+
var i ImagingConfigInternal
if err := mapstructure.Decode(m, &i.Imaging); err != nil {
return i, nil, err
}
if c.Hint == "" {
- c.Hint = "photo"
+ c.Hint = defaults.Config.Imaging.Webp.Hint
}
if c.Action != "" && c.Anchor == -1 {
Compression string
+ // WebP-specific options.
+ UseSharpYuv bool
+ Method int
+
Width int
Height int
// Currently only used when encoding to Webp.
// Default is "photo".
// Valid values are "picture", "photo", "drawing", "icon", or "text".
- Hint string
+ // Moved to WebpConfig in v0.155.0, but kept here for backwards compatibility.
+ Hint string `json:"-"`
// The anchor to use in Fill. Default is "smart", i.e. Smart Crop.
Anchor string
Exif ExifConfig
Meta MetaConfig
+ Webp WebpConfig
}
var validMetaSources = map[string]bool{
}
}
+ // WebP config with backwards compatibility for root-level Hint.
+ cfg.Webp.Hint = strings.ToLower(cfg.Webp.Hint)
+ if cfg.Webp.Hint == "" {
+ // Fall back to root-level hint for backwards compatibility.
+ if cfg.Hint != "" {
+ cfg.Webp.Hint = cfg.Hint
+ } else {
+ cfg.Webp.Hint = defaultHint
+ }
+ }
+ if cfg.Webp.Hint != "" && !hints[cfg.Webp.Hint] {
+ return fmt.Errorf("invalid webp hint %q; must be one of picture, photo, drawing, icon, or text", cfg.Webp.Hint)
+ }
+ if cfg.Webp.Method == 0 {
+ cfg.Webp.Method = defaultWebpMethod
+ }
+ if cfg.Webp.Method < 0 || cfg.Webp.Method > 6 {
+ return fmt.Errorf("webp method must be between 0 and 6, got %d", cfg.Webp.Method)
+ }
+
return nil
}
// Default is ["exif", "iptc"] (XMP is excluded for performance reasons).
Sources []string
}
+
+// WebpConfig holds WebP-specific encoding configuration.
+type WebpConfig struct {
+ // Hint about what type of image this is.
+ // Valid values are "picture", "photo", "drawing", "icon", or "text".
+ // Default is "photo".
+ Hint string
+
+ // Use sharp (and slow) RGB->YUV conversion.
+ // Default is true.
+ UseSharpYuv bool
+
+ // Quality/speed trade-off (0=fast, 6=slower-better).
+ // Default is 2.
+ Method int
+}
}
return ImageConfig{
Anchor: -1, // The real values start at 0.
- Hint: "photo",
+ Hint: defaults.Config.Imaging.Webp.Hint,
Quality: defaults.Config.Imaging.Quality,
Compression: defaults.Config.Imaging.Compression,
+ UseSharpYuv: defaults.Config.Imaging.Webp.UseSharpYuv,
+ Method: defaults.Config.Imaging.Webp.Method,
}
}
c.Assert(err, qt.IsNil)
c.Assert(len(entries1), qt.Equals, len(entries2))
for i, e1 := range entries1 {
- if shouldSkip != nil && shouldSkip(e1) {
+ if shouldSkip(e1) {
continue
}
c.Assert(filepath.Ext(e1.Name()), qt.Not(qt.Equals), "")
if err != nil {
panic(fmt.Sprintf("failed to decode image: %s", err))
}
+
gift.New().Draw(dst, src)
gift.New().DrawAt(dst, overlaySrc, image.Pt(f.x, f.y), gift.OverOperator)
}