Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
98 changes: 97 additions & 1 deletion src/common/exif.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
TurboGit marked this conversation as resolved.

// 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)
{
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/common/image.c
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
4 changes: 4 additions & 0 deletions src/common/image.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
61 changes: 61 additions & 0 deletions src/iop/crop.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading