Image
- Version
- 4.0 preview
- Updated
An uploaded image: the file goes on disk under dirBase, the filename goes in the column, and
any number of derived versions — thumbnails, web-optimised copies, square crops — are generated
alongside it at upload time.
Storage kind: column · Column: varchar(255)
Use it for anything displayed as a picture that benefits from resizing: product images, answer
images, question images, the shop logo. For a downloadable asset that is never resized — a PDF
datasheet, a CAD file — use file, which is the same machinery without the image
processing.
Settings
| Setting | Meaning |
|---|---|
dirBase | Required. Filesystem directory the file is written to. Created (recursively, 0777) if missing |
urlBase | URL base the file is served from. Without it, reads expose no _href |
filename | Fixed base filename, instead of the uploaded one |
appendSerial | Append a random 4-digit serial to the base name — a cache-buster, see below |
allowedExtensions | Array of permitted extensions, lower-case, without the dot |
allowedMimeTypes | Array of permitted MIME types — checked as well as the extension |
maxFileSizeKb | Upload ceiling in KB. Was size; a shim still routes the old key |
minimumDimensions | array('width' => …, 'height' => …). Either key may be omitted |
mutations | Derived versions to generate — see Mutations |
options | FILENAME_TO_RECORD_ID, NODELETEFILE, NO_SAVE_FILENAME — see below |
A representative definition, from adminproducts:
$propDefs['image'] = array(
'name' => 'image',
'label' => KText::_('Product Image'),
'type' => 'image',
'appendSerial' => 1,
'allowedExtensions' => array('jpg', 'jpeg', 'gif', 'tif', 'bmp', 'png'),
'allowedMimeTypes' => array('image/jpeg', 'image/gif', 'image/png', /* … */),
'maxFileSizeKb' => '2000',
'dirBase' => KenedoPlatform::p()->getDirDataStore().'/public/product_images',
'urlBase' => KenedoPlatform::p()->getUrlDataStore().'/public/product_images',
'options' => 'FILENAME_TO_RECORD_ID',
'positionForm' => 90000,
);
The filename is persisted by default
store() calls updateOriginalFileName() unless the definition opts out with NO_SAVE_FILENAME.
This used to be opt-in, behind a SAVE_FILENAME tag, and getting it wrong failed silently: the file
was still uploaded, validated and mutated, but the column was never updated — and since
prepareForStorage() has already written the previous filename into the data object, the base row
kept pointing at the old file, or at nothing. No error, nothing in a log, just a form that comes back
empty. Every shipped image definition set the tag, which is the clearest evidence it was not a real
choice.
NO_SAVE_FILENAME remains for the genuine case the old flag was invented for: a pure upload widget
whose file location is derived from the record id, where storing the name is redundant. Note that
even the shipped FILENAME_TO_RECORD_ID definitions persisted the name anyway.
How the filename is built
In getNewOriginalFilename(), the base name is the first of these that applies:
FILENAME_TO_RECORD_IDinoptions→ the record id.filenamein the definition → that literal string.- Otherwise → the uploaded name, lower-cased, with its extension stripped.
Then, if appendSerial is set, - plus str_pad(rand(1,1000), 4, 0) is appended. Finally the
uploaded file's extension is put back. The extension is always the uploaded one — no setting
changes it. (PRESERVE_EXT, which nine shipped definitions used to carry, was read by nothing and
has been removed.)
appendSerial exists to defeat browser and CDN caches: when the operator replaces a product image
the new file has a different name, so nothing serves the stale one. The old file is unlinked once
the new one is in place. Note the serial is random, not sequential, and str_pad pads on the
right — rand() returning 7 gives 7000, not 0007. It is a cache-buster, not an ordering and
not a collision-proof unique suffix.
Validation
check() collects every problem with one file and throws them together, rather than reporting
the first and making the operator upload again to learn about the next:
| Checked | Condition |
|---|---|
| Presence | Only when required and appliesWhen says the field applies, and only if there is no current file either |
maxFileSizeKb | filesize() of the temp file against the ceiling |
allowedMimeTypes | KenedoFileHelper::getMimeType(). Skipped silently if the system cannot determine a MIME type |
allowedExtensions | The uploaded name's extension |
minimumDimensions | Only if everything above passed — no point measuring a rejected non-image |
Declare both allowedExtensions and allowedMimeTypes. An extension is a claim by the client;
the MIME check looks at what was actually sent. Neither is sufficient alone, and the MIME check is
the one that can silently not run.
A file whose dimensions getimagesize() cannot read is refused, not measured against nothing.
That is deliberate: the code previously set a message and then compared against null anyway, so an
unreadable file passed the size checks it should have failed.
Mutations
A mutation is a derived image generated from the upload — a thumbnail, a web-optimised copy, a square crop. They are declared as a map of name → recipe:
'mutations' => array(
'thumb' => array(
'mode' => 'coverAndCrop',
'params' => array('width' => 200, 'height' => 200),
),
'web' => array(
'mode' => 'contain',
'params' => array('width' => 1200, 'outputFormat' => 'webp', 'outputQuality' => 80),
),
),
The key (thumb, web) names the mutation and appears both in the generated filename and in the
record keys — pick something stable and URL-safe.
No shipped property definition declares mutations; the feature is there for products and customizations that need sized variants.
Modes
mode | What it does | Aspect ratio | Params used |
|---|---|---|---|
coverAndCrop | Fills the target box exactly, cropping the overflow, anchored center | preserved, excess cut | width, height (both required) |
contain | Scales to fit inside the target box; one dimension ends up shorter | preserved, nothing cut | width and/or height |
cover | Scales to fill the target box; one dimension ends up longer, nothing is cropped | preserved, nothing cut | width and/or height |
forceRatioAndContain is a deprecated alias for coverAndCrop and is still accepted. Any other
mode throws at upload time, naming the model and property — a typo here fails the save rather
than silently skipping the mutation.
Use coverAndCrop for anything that must tile in a fixed grid (product-list thumbnails); contain
when the whole image must stay visible and the container can absorb the leftover space.
Known defect:
covercurrently does not scale at all.getImageCoveredWithoutCrop()reads a variable that is not in scope and so overwrites both target dimensions withnull, returning the image untouched. UsecontainorcoverAndCropuntil that is fixed.
Output format and quality
| Param | Meaning |
|---|---|
outputFormat | webp | jpg | png | gif. Omitted means keep the original's extension |
outputQuality | 1–100, default 70. Passed to toWebp() and toJpeg() only — the png and gif conversions are called without it |
Every mutation is converted to the sRGB colourspace and has its colour profile removed before saving, so a CMYK or wide-gamut upload does not produce browser-dependent colours in its derivatives.
Processing runs on Intervention Image, using the Imagick driver when that extension is loaded and falling back to GD otherwise. Output can differ slightly between the two — a property of the host, not of your definition.
Mutation filenames
<base>-<mutationName>[-<serial>].<ext>
<base> and <serial> come from the original, split apart on the last - when appendSerial is
on; <ext> is outputFormat or the original's extension. So chair-0420.jpg with a thumb
mutation becomes chair-thumb-0420.jpg, and with 'outputFormat' => 'webp', chair-thumb-0420.webp.
When reading, the property first looks for the expected extension and, failing that, probes jpg,
gif, png, webp in turn. That is what lets you change a mutation's outputFormat without
orphaning the files already generated under the old one.
Lifecycle
| Event | What happens to the mutations |
|---|---|
| Upload | Generated immediately, in store(), one per entry in mutations |
| Replace | The new upload's mutations are generated; the previous original and its mutations are unlinked |
| Delete checkbox | Original and mutations unlinked; the column is cleared |
| Record delete | Original and mutations unlinked before the row goes |
| Record copy | Original and every existing mutation are copied under the new name |
| Adding a mutation to the definition | Nothing. Existing records keep the mutations they were uploaded with |
That last row is the one that bites. Mutations are generated at upload, never on demand, so adding
one to a live definition produces it for new uploads only. There is a recreateMutations() method
that walks the model's records and regenerates, but the source marks it experimental — not to be
used yet; do not treat it as the supported backfill path. Re-saving the affected records with a
fresh upload is what actually works today.
What reads produce
For a property named image with a thumb mutation, appendDataForGetRecord() adds:
| Key | Holds | Present when |
|---|---|---|
image | the filename | always |
image_href | urlBase + / + filename | urlBase is set and the filename is non-empty |
image_path | dirBase + / + filename | dirBase is set and the filename is non-empty |
image_thumb_href | the mutation's URL | the mutation file exists on disk |
image_thumb_path | the mutation's filesystem path | the mutation file exists on disk |
The mutation keys are driven by what is on disk, not by what the definition declares — so a record uploaded before a mutation was added simply has no key for it. Consumers must treat them as optional; there is no placeholder.
That underscore is deliberate and always was: image and file were already following the
convention that translatable was not.
The delete checkbox
The template renders a "delete file at save" checkbox only when all of these hold: a file is
stored, NODELETEFILE is not in options, the property is not required, and the record id is not
0. It posts <propertyName>-delete=1, which store() reads straight from the request.
If that flag arrives when the property is not deletable, store() throws rather than ignoring
it — a delete arriving for a required field means something is wrong with the form, not with the
operator's intent.
Notes
- The upload is
copy()d from the temp file, notrename()d the wayfiledoes it. Permissions on the destination are set to0775. storeExternallymoves the filename column to another table; the property implements the branch itself ingetCurrentOriginalFileName()andupdateOriginalFileName(). The files themselves always live underdirBase. See the overview'sstoreExternallysection.- Copying a record whose source file is missing on disk throws, aborting the copy.
fileonly logs a warning and carries on. If you copy records whose images may have been removed outside the admin, that difference matters. - Export/import (
getPropertyFilePaths()) carries the original only. Mutations are regenerated on import rather than transferred.
@see file.md
@see ../com_configbox_property_types.md §3 for storeExternally
@see ../../admin-guide/products/add-a-product.md — the product image from the operator's side