Skip to content

PLIP: Per-scale mode (crop) in plone.allowed_sizes #4291

@jensens

Description

@jensens

PLIP (Plone Improvement Proposal)

Responsible Persons

Proposer: Jens W. Klein (@jensens)

Seconder:

Abstract

Extend plone.allowed_sizes text format to support an optional scaling mode per named scale.

Current format: name width:height
Proposed format: name width:height[:mode]

Where mode is scale (default, can be omitted) or crop (fill exact dimensions, cut off overflow).

This enables configuring cropped scales through the Image Handling Settings control panel — something currently impossible with named scales.

Motivation

ImageScaling.scale() accepts a mode parameter — but when a named scale is used (e.g. @@images/image/preview), the mode is always hardcoded to "scale":

https://github.com/plone/plone.namedfile/blob/master/plone/namedfile/scaling.py#L598

width, height = available[scale]

getAllowedSizes() returns {name: (width, height)} — no mode.

There is no way to configure a named scale with crop behavior through the control panel or registry. The only option is explicit Python API calls with width/height/mode parameters.

Real-world use case: image galleries, teasers, and card layouts need exact square crops (400×400 crop, not 400×400 fit-within). Currently impossible without custom code.

Naming confusion

The existing mode names contain and cover are documented as aligned with CSS background-size but actually have swapped semantics compared to CSS:

  • Plone's contain crops. CSS contain does not crop.
  • Plone's cover does not crop. CSS cover crops.

The proposed crop term avoids this confusion entirely. It means what it says: fill exact dimensions, cut off the rest.

Assumptions

  • plone.allowed_sizes is a free-text lines field — the format can be extended without schema changes.
  • Only two modes matter in practice: scale (fit within box) and crop (fill exact dimensions). contain and cover exist but their naming is confusing (see above).
  • Backward compatible: lines without :mode suffix keep working as before (default scale).

Proposal & Implementation

1. Text format

preview 400:400:crop      ← crop to exact 400×400
large 800:600             ← scale (default, no suffix needed)
thumb 128:128:scale       ← explicit scale (same as no suffix)

2. Changes in plone.namedfile

utils/__init__.py — extend regex, return 3-tuple:

# current
pattern = re.compile(r"^(.*)\s+(\d+)\s*:\s*(\d+)$")

# proposed
pattern = re.compile(r"^(.*)\s+(\d+)\s*:\s*(\d+)(?:\s*:\s*(\w+))?$")

getAllowedSizes() returns {name: (width, height, mode)} where mode defaults to "scale".

scaling.py — unpack mode from available sizes:

# current (line 598)
width, height = available[scale]

# proposed
width, height, mode = available[scale]

3. Mode mapping

Text format Internal mode= Behavior
(default / scale) "scale" Fit within box, preserve aspect ratio
crop "contain" Fill exact dimensions, crop overflow

cover and contain accepted as aliases for backward compat.

4. Callers to update

  • plone/namedfile/scaling.py:598 — unpack 3-tuple, use mode
  • plone/app/vocabularies/images.py:19for scale, (width, height) in ... → add mode

Deliverables

  • plone.namedfile — format parsing, getAllowedSizes(), scale() mode passthrough
  • plone.app.vocabularies — adapt tuple unpacking
  • plone.restapi — adapt scale info handling (backward compatible with older Plone versions):
    • split_scale_info() in imaging.py parses the mode from raw plone.allowed_sizes text lines directly (does not depend on getAllowedSizes()), defaults to "scale" when absent — works on any Plone version
    • get_scales() passes mode to images_view.scale()
    • get_actual_scale() accounts for crop dimensions
    • @site endpoint already exposes raw plone.allowed_sizes strings (no change needed)
  • documentation — update docs/classic-ui/images.md:
    • Document the extended format name width:height[:mode]
    • Add crop to the scaling modes reference
    • Clarify the contain/cover naming vs CSS conventions
  • Volto — no changes required (only uses download, width, height from scale objects; unknown fields are ignored)
  • No control panel UI changes needed (textarea already accepts free text)
  • No migration needed (old format still valid)

Risks

Low. Backward compatible — existing name width:height lines work unchanged.

Only breakage risk: third-party code calling getAllowedSizes() and destructuring as 2-tuple. Unlikely outside core.

Participants

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    No status

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions