Migrating cropped images
James Williams
One of our big Drupal 7 to Drupal 9 migration projects included bringing across image cropping functionality and data on a longstanding client's website. This site had used the Imagefield Crop module, but that was only for Drupal 7. We set up Image Widget Crop for the new site, which is better in a few ways but is also fundamentally different. The old site referenced the cropped images from content, only bringing in the originally-uploaded images for edit pages, to allow editors to adjust the cropping, which was then used wherever that image appeared on the frontend. The new Image Widget Crop module, however, allows configuring different crops for different situations. For example, the same image could be cropped one way for use as a widescreen banner, but another way for when it appears in a grid of squares (as in the following screenshot).
The real challenge was in migrating the data! But we call ourselves Drupal experts, so of course we dug to find solutions. What we came up with might not work for everyone, but hopefully sharing this might help someone else that might be close enough to our situation. We found the following steps were necessary...
1. Set up the new configuration
Configure the crop types, cropping settings, fields and widgets etc for the Drupal 9 site to use, including an image media type. I won't go into detail here as there are already guides about how to do this - e.g. from the Drupal Media Team and OSTraining. What I'm focussed on is how to migrate the cropped images and their original files, and the references to use them in the right places.
2. Migrate files, including media entities, and references to them
The old site's files can be migrated into the new site easily enough. I then use the Media Migration module to create media image entities that reference those files. In my situation, it was fine to migrate the file IDs over and to use matching media IDs too. I expect this won't be an option on some projects but it made things much easier for me.
The Media Migration module uses 'dealer' plugin classes to cover different types of media, but its 'image' dealer plugin ignores images handled by Drupal 7's Imagefield Crop module. So I had to replace that with a custom plugin.
In general, I aimed to migrate the originally-uploaded image files (i.e. from before cropping was applied), and reference those from host content. That's how the new Image Widget Crop module usually references the images, whereas the Imagefield Crop module referenced the cropped image files. The Image Widget Crop module usually maps to the cropped images via the chosen 'crop type' referenced in image styles so that different crops can be used for the same field when output in different places. Therefore, any migrations for it will have to translate back from the IDs of cropped image files to the IDs for 'uncropped' ones.
A custom module's hook_migrate_prepare_row()
did that file ID translation, and also skipped migrating cropped images as media entities. Since the cropped images won't be referenced from content, they would just clog up the media library as duplicates of the original uncropped images. Detecting which files were only cropped images that wouldn't be referenced from elsewhere was a bit tricky, and one of the slowest parts of my migrations. So I allowed file entities to be created for these cropped images, as I figured that didn't matter so much. I imagine these two bits could have been done better with specific plugin classes rather than this hook.
For migrating the right data into the media field on every node/entity that referenced croppable images, I made a custom process plugin to make use of that mapping of cropped-to-uncropped file IDs. So my node migration YAML files declared this plugin should be used to get the uncropped file ID on the destination, that corresponded to the cropped images' IDs on the source end, like this:
field_image:
plugin: MYMODULE_precropped_image
source: field_image
That plugin basically just looks up the ID of the cropped file from the translation map that was set by the hook_migrate_prepare_row()
and returns the ID of the uncropped image file.
3. Migrate cropping data
I needed additional migrations for the data in every Imagefield Crop field about the dimensions and positioning of the crops themselves. These created 'crop' entities, using a custom source plugin for a database table that extended Drupal\migrate\Plugin\migrate\source\SqlBase
. This allowed me to use a bundle filter in my migrations, so different crop types could be used in Drupal 9 for images on different content types that happened to use the same source field storage in Drupal 7. The YAML for these migrations is simple enough to share:
langcode: en
status: true
dependencies:
module:
- crop
- MYMODULE
id: d7_crop_field_image
class: Drupal\migrate\Plugin\Migration
migration_tags:
- Content
migration_group: migrate_drupal_7
label: 'Image crops from field_image, except for galleries'
source:
plugin: MYMODULE_imagefield_crop_table
field_name: field_image
bundle_filter:
- article
- blog_post
constants:
crop_type: 4_3_landscape
target_entity_type: file
process:
type: constants/crop_type
# Our source plugin sets precropped_fid.
entity_id: precropped_fid
entity_type: constants/target_entity_type
uri: uri
height: field_image_cropbox_height
width: field_image_cropbox_width
x:
plugin: callback
# The D7 module recorded the co-ordinate of the top left of the crop box,
# whereas we want the co-ordinate of the very centre of the crop box.
callable: MYMODULE_translate_crop_coordinate
unpack_source: true
source:
- field_image_cropbox_x
- field_image_cropbox_width
'y':
plugin: callback
callable: MYMODULE_translate_crop_coordinate
unpack_source: true
source:
- field_image_cropbox_y
- field_image_cropbox_height
destination:
plugin: 'entity:crop'
4. Mitigate missing features in the new site
The old and new modules have their own different approaches, which means they don't quite have feature parity. I actually think the new Image Widget Crop module has a better overall approach but our old site did make use of some settings unique to Imagefield Crop which we wanted to bring across. Most interestingly, as many of the old site's image fields were configured to use the same dimensions or aspect ratios, we could set up crops on the new site to be shared across those fields. However, there were a few fields that had slightly different constraints on 'input' despite appearing the same on 'output'. So we had to alter Image Widget Crop's widget (via two levels of #process
Form API callbacks!) to apply different maximum post-cropping dimensions for certain contexts. Editors could bypass this fairly easily, but it covers the most common journeys that they would normally follow.
Mission accomplished!
After all that, you can probably see it was quite a complex challenge! A lot of code went into this, which I have shared barely any of here but if you find yourself in a similar scenario and need help, get in touch or leave a comment here. If anyone actually needs the PHP or YAML code, maybe I can look at packaging it up to share. But I know it's probably not generic enough to cover everyone's situations - I'd love to contribute it back to the community otherwise.