diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 7ce35d9a27b..d717d2836ea 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -138,6 +138,11 @@ changes (where available). - pixelpipe dump files requested via cli switches are now written in ppm or pgm format. +- The aspect ratio chosen on the camera is now applied as a crop when + the raw was left uncropped, so a frame shot at 1:1 or 16:9 opens + framed as intended while the full sensor area stays available to + reframe within. Read from Canon and Olympus raws. + ## Bug Fixes - Clarified multi-image rating toasts for un-reject and for mixed diff --git a/src/common/exif.cc b/src/common/exif.cc index 7bb3332805e..68576c259c5 100644 --- a/src/common/exif.cc +++ b/src/common/exif.cc @@ -956,6 +956,99 @@ static bool _check_usercrop(Exiv2::ExifData &exifData, return FALSE; } +// Support the aspect ratio dialed in on the camera while shooting. Every +// vendor keeps it somewhere else in its own makernote, but what we need out +// of it is the same everywhere: the ratio, and whether it differs from the +// native one at all. +// +// The box a camera reports is always centered on the output area of that +// camera, which is a little smaller than the sensor area we process. We +// therefore keep the ratio alone and leave the centering to the crop module, +// so we never have to match the vendor's idea of the full frame. +// +// To support another vendor, write a reader filling d:n and add it below. + +// Canon keeps it in AspectInfo as (ratio, width, height, left, top). +// Magic-nr taken from the makernote spec, Exiv2 knows the tag by name +// only from 0.27.4 on. +static bool _camera_aspect_canon(Exiv2::ExifData &exifData, + int *d, + int *n) +{ + Exiv2::ExifData::const_iterator pos = + exifData.findKey(Exiv2::ExifKey("Exif.Canon.0x009a")); + + if(pos == exifData.end() || pos->count() != 5 || !pos->size()) + return FALSE; + + // a box sitting at the origin covers the full frame, nothing was cropped + if(pos->toFloat(3) <= 0.0f && pos->toFloat(4) <= 0.0f) + return FALSE; + + switch((int)pos->toFloat(0)) + { + case 1: *d = 1; *n = 1; break; // 1:1 + case 2: *d = 4; *n = 3; break; // 4:3 + case 7: *d = 16; *n = 9; break; // 16:9 + case 12: *d = 185; *n = 100; break; // 1.85:1 + default: return FALSE; // 0 is native, the rest is unknown + } + + return TRUE; +} + +// Olympus keeps it in the image processing block as two bytes: the ratio, +// and whether the raw data was cropped along with it. Only the second form +// concerns us, the other one has nothing left to reproduce. +static bool _camera_aspect_olympus(Exiv2::ExifData &exifData, + int *d, + int *n) +{ + Exiv2::ExifData::const_iterator pos = + exifData.findKey(Exiv2::ExifKey("Exif.OlympusIp.0x1112")); + + if(pos == exifData.end() || pos->count() != 2 || !pos->size()) + return FALSE; + + // the second byte tells the raw data was left uncropped + if((int)pos->toFloat(1) != 1) + return FALSE; + + switch((int)pos->toFloat(0)) + { + case 2: *d = 3; *n = 2; break; // 3:2 + case 3: *d = 16; *n = 9; break; // 16:9 + case 4: *d = 1; *n = 1; break; // 1:1 + default: return FALSE; // 1 is the native 4:3, the rest is unknown + } + + return TRUE; +} + +static bool _check_camera_aspect(Exiv2::ExifData &exifData, + dt_image_t *img) +{ + static bool (*const readers[])(Exiv2::ExifData &, int *, int *) = + { + _camera_aspect_canon, + _camera_aspect_olympus, + }; + + for(size_t i = 0; i < sizeof(readers) / sizeof(readers[0]); i++) + { + int d = 0, n = 0; + + if(readers[i](exifData, &d, &n) && d > 0 && n > 0) + { + img->camera_ratio_d = d; + img->camera_ratio_n = n; + return TRUE; + } + } + + return FALSE; +} + static void _check_linear_response_limit(Exiv2::ExifData &exifData, dt_image_t *img) { @@ -1404,6 +1497,7 @@ void dt_exif_img_check_additional_tags(dt_image_t *img, if(!exifData.empty()) { _check_usercrop(exifData, img); + _check_camera_aspect(exifData, img); _check_dng_opcodes(exifData, img); _check_lens_correction_data(exifData, img); _check_linear_response_limit(exifData, img); @@ -1719,7 +1813,9 @@ static bool _exif_decode_exif_data(dt_image_t *img, Exiv2::ExifData &exifData) } } - if(_check_usercrop(exifData, img)) + // both are read, so no short-circuiting here + const bool has_usercrop = _check_usercrop(exifData, img); + if(_check_camera_aspect(exifData, img) || has_usercrop) { img->flags |= DT_IMAGE_HAS_ADDITIONAL_EXIF_TAGS; guint tagid = 0; diff --git a/src/common/image.c b/src/common/image.c index c3ba33c43e7..d7e31943651 100644 --- a/src/common/image.c +++ b/src/common/image.c @@ -2254,6 +2254,7 @@ void dt_image_init(dt_image_t *img) img->wb_coeffs[3] = NAN; img->usercrop[0] = img->usercrop[1] = 0; img->usercrop[2] = img->usercrop[3] = 1; + img->camera_ratio_d = img->camera_ratio_n = 0; img->dng_gain_maps = NULL; img->exif_correction_type = CORRECTION_TYPE_NONE; memset(&img->exif_correction_data, 0, sizeof(img->exif_correction_data)); diff --git a/src/common/image.h b/src/common/image.h index b889c4e5963..546af1217cf 100644 --- a/src/common/image.h +++ b/src/common/image.h @@ -358,6 +358,10 @@ typedef struct dt_image_t /* DefaultUserCrop */ dt_boundingbox_t usercrop; + /* aspect ratio dialed in on the camera while shooting, as d:n following + the crop module convention; 0:0 if the native sensor ratio was used */ + int32_t camera_ratio_d, camera_ratio_n; + /* GainMaps from DNG OpcodeList2 exif tag */ GList *dng_gain_maps; diff --git a/src/iop/crop.c b/src/iop/crop.c index 1c6df871c65..a975fd61c64 100644 --- a/src/iop/crop.c +++ b/src/iop/crop.c @@ -889,6 +889,67 @@ void reload_defaults(dt_iop_module_t *self) dp->cw = img->usercrop[3]; dp->ch = img->usercrop[2]; dp->ratio_n = dp->ratio_d = -1; + + self->default_enabled = FALSE; + + // A crop coming from the raw only preloads our parameters. An aspect ratio + // dialed in on the camera though is a framing decision taken while + // shooting, so reproduce it right away: center that ratio on the sensor + // area we are handed and switch ourselves on. The intended framing is + // applied but stays free to be moved around within the full frame. + const gboolean has_usercrop = dp->cx > 0.0f || dp->cy > 0.0f + || dp->cw < 1.0f || dp->ch < 1.0f; + + const float iwd = img->p_width > 0 ? img->p_width : img->width; + const float iht = img->p_height > 0 ? img->p_height : img->height; + + if(has_usercrop + || img->camera_ratio_d < 1 + || img->camera_ratio_n < 1 + || iwd < 1.0f + || iht < 1.0f) + return; + + // p_width and p_height are still in sensor orientation here + const float want = (float)img->camera_ratio_d / (float)img->camera_ratio_n; + const float have = iwd / iht; + + float bx = 0.0f, by = 0.0f, bw = 1.0f, bh = 1.0f; + if(want < have) // narrower than the sensor, take off left and right + { + bw = want / have; + bx = (1.0f - bw) * 0.5f; + } + else if(want > have) // wider, take off top and bottom + { + bh = have / want; + by = (1.0f - bh) * 0.5f; + } + + // The ratio is the one we are handed already, either because the vendor + // applied the crop to the raw data after all or because the value was read + // as something it is not. Nothing to reproduce, and staying off is the safe + // answer rather than adding a crop that does nothing. + if(bw > 0.995f && bh > 0.995f) + return; + + // flip runs before us, so follow the image into portrait. The box is + // centered, which leaves the mirroring bits of the orientation a no-op. + if(dt_image_orientation(img) & ORIENTATION_SWAP_XY) + { + float t; + t = bx; bx = by; by = t; + t = bw; bw = bh; bh = t; + } + + dp->cx = bx; + dp->cy = by; + dp->cw = bx + bw; + dp->ch = by + bh; + dp->ratio_d = img->camera_ratio_d; + dp->ratio_n = img->camera_ratio_n; + + self->default_enabled = TRUE; } static void _float_to_fract(const char *num, int *n, int *d)