summaryrefslogtreecommitdiff
path: root/src/android
AgeCommit message (Expand)Author
2021-02-05android: camera_device: Report AE_EXPOSURE_COMPENSATIONJacopo Mondi
2021-02-05android: camera_device: Report COLOR_CORRECTION_ABERRATION_MODEJacopo Mondi
2021-02-04android: camera_device: Generate JPEG thumbnail sizesJacopo Mondi
2021-02-04android: camera_device: Fix wrong commentJacopo Mondi
2021-02-04android: Try build with a local libyuv firstHirokazu Honda
2021-02-04subprojects: Add libyuv and built if -Dandroid=enabledHirokazu Honda
2021-02-02android: post_processor: Change the type destination in process()Hirokazu Honda
2021-02-02libcamera: Improve Request life cycle trackingJacopo Mondi
2021-02-02android: camera_device: Use reference to settingsJacopo Mondi
2021-02-02android: camera_device: Add missing request keysJacopo Mondi
2021-02-02android: camera_device: Alphabetically sort keysJacopo Mondi
2021-02-01android: camera_device: Fix exposure time tag in exif and androidPaul Elder
2021-01-29android: camera_device: Set AE precapture trigger according to requestPaul Elder
2021-01-27android: jpeg: Set thumbnail and JPEG quality based on requestPaul Elder
2021-01-27android: camera_device: Cache request metadataPaul Elder
2021-01-27android: jpeg: Configure thumbnailer based on request metadataPaul Elder
2021-01-27android: Set result metadata and EXIF fields based on request metadataPaul Elder
2021-01-27android: camera_device: Load make and model from platform settingsPaul Elder
2021-01-27android: jpeg: exif: Add functions for setting various valuesPaul Elder
2021-01-27android: jpeg: exif: Fix setOrientation EXIF valuesPaul Elder
2021-01-27android: jpeg: exif: Expand setString to support different encodingsPaul Elder
2021-01-25android: camera_device: Report the required dynamic metadataJacopo Mondi
2021-01-25android: camera_device: Support AWB_AUTOJacopo Mondi
2021-01-25android: camera_device: Handle SCALER_CROP_REGIONJacopo Mondi
2021-01-25android: camera_device: Register MAX_DIGITAL_ZOOMJacopo Mondi
2021-01-25android: camera_device: Report EXPOSURE_TIMEJacopo Mondi
2021-01-25android: camera_device: Register EXPOSURE_TIME_RANGEJacopo Mondi
2021-01-22android: camera_device: Clone settings in request descriptorJacopo Mondi
2021-01-22android: camera_metadata: Add defaul constructorJacopo Mondi
2021-01-22android: camera_device: Copy camera3 buffers in descriptorJacopo Mondi
2021-01-22android: camera_device: Pass camera3 request to descriptorJacopo Mondi
2021-01-22android: camera_metadata: Add copy constructor and getEntryPaul Elder
2021-01-18android: camera_device: Do not default pixel array propertiesJacopo Mondi
2021-01-04android: camera_device: Only advertise RAW support if RAW16 is availableNiklas Söderlund
2020-12-30android: camera_device: Simplify properties.get()Jacopo Mondi
2020-12-30android: camera_device: Report ColorFilterArrangementJacopo Mondi
2020-12-18android: camera_device: Report number of out streamsJacopo Mondi
2020-12-18android: camera_device: Report pipeline depthJacopo Mondi
2020-12-14android: camera_device: Replace auto with explicit typeLaurent Pinchart
2020-12-14android: camera_device: Temporary fix sensor propertiesJacopo Mondi
2020-12-11android: camera_device: Reorder configurations before requestingHirokazu Honda
2020-12-11android: camera_device: Use Camera3StreamConfig in configureStreams()Hirokazu Honda
2020-12-11android: camera_device: Introduce Camera3StreamConfigHirokazu Honda
2020-12-07android: camera_device: Stop camera when re-configuring itJacopo Mondi
2020-12-07android: camera_device: Initialize pixel array propertiesJacopo Mondi
2020-12-02android: camera_device: Check capture_request validityJacopo Mondi
2020-12-02android: camera_mode: Reserve 'data' vectorsJacopo Mondi
2020-11-17android: jpeg: Use LGPL-2.1 license for post_processor_jpeg.cppLaurent Pinchart
2020-10-28android: jpeg: post_processor_jpeg: Embed thumbnail into Exif metadataUmang Jain
2020-10-28android: jpeg: Introduce a simple image thumbnailerUmang Jain
href='#n421'>421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618
/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
 * Copyright (C) 2020, Raspberry Pi Ltd
 *
 * dng_writer.cpp - DNG writer
 */

#include "dng_writer.h"

#include <algorithm>
#include <iostream>
#include <map>

#include <tiffio.h>

#include <libcamera/control_ids.h>
#include <libcamera/formats.h>
#include <libcamera/property_ids.h>

using namespace libcamera;

enum CFAPatternColour : uint8_t {
	CFAPatternRed = 0,
	CFAPatternGreen = 1,
	CFAPatternBlue = 2,
};

struct FormatInfo {
	uint8_t bitsPerSample;
	CFAPatternColour pattern[4];
	void (*packScanline)(void *output, const void *input,
			     unsigned int width);
	void (*thumbScanline)(const FormatInfo &info, void *output,
			      const void *input, unsigned int width,
			      unsigned int stride);
};

struct Matrix3d {
	Matrix3d()
	{
	}

	Matrix3d(float m0, float m1, float m2,
		 float m3, float m4, float m5,
		 float m6, float m7, float m8)
	{
		m[0] = m0, m[1] = m1, m[2] = m2;
		m[3] = m3, m[4] = m4, m[5] = m5;
		m[6] = m6, m[7] = m7, m[8] = m8;
	}

	Matrix3d(const Span<const float> &span)
		: Matrix3d(span[0], span[1], span[2],
			   span[3], span[4], span[5],
			   span[6], span[7], span[8])
	{
	}

	static Matrix3d diag(float diag0, float diag1, float diag2)
	{
		return Matrix3d(diag0, 0, 0, 0, diag1, 0, 0, 0, diag2);
	}

	static Matrix3d identity()
	{
		return Matrix3d(1, 0, 0, 0, 1, 0, 0, 0, 1);
	}

	Matrix3d transpose() const
	{
		return { m[0], m[3], m[6], m[1], m[4], m[7], m[2], m[5], m[8] };
	}

	Matrix3d cofactors() const
	{
		return { m[4] * m[8] - m[5] * m[7],
			 -(m[3] * m[8] - m[5] * m[6]),
			 m[3] * m[7] - m[4] * m[6],
			 -(m[1] * m[8] - m[2] * m[7]),
			 m[0] * m[8] - m[2] * m[6],
			 -(m[0] * m[7] - m[1] * m[6]),
			 m[1] * m[5] - m[2] * m[4],
			 -(m[0] * m[5] - m[2] * m[3]),
			 m[0] * m[4] - m[1] * m[3] };
	}

	Matrix3d adjugate() const
	{
		return cofactors().transpose();
	}

	float determinant() const
	{
		return m[0] * (m[4] * m[8] - m[5] * m[7]) -
		       m[1] * (m[3] * m[8] - m[5] * m[6]) +
		       m[2] * (m[3] * m[7] - m[4] * m[6]);
	}

	Matrix3d inverse() const
	{
		return adjugate() * (1.0 / determinant());
	}

	Matrix3d operator*(const Matrix3d &other) const
	{
		Matrix3d result;
		for (unsigned int i = 0; i < 3; i++) {
			for (unsigned int j = 0; j < 3; j++) {
				result.m[i * 3 + j] =
					m[i * 3 + 0] * other.m[0 + j] +
					m[i * 3 + 1] * other.m[3 + j] +
					m[i * 3 + 2] * other.m[6 + j];
			}
		}
		return result;
	}

	Matrix3d operator*(float f) const
	{
		Matrix3d result;
		for (unsigned int i = 0; i < 9; i++)
			result.m[i] = m[i] * f;
		return result;
	}

	float m[9];
};

void packScanlineSBGGR10P(void *output, const void *input, unsigned int width)
{
	const uint8_t *in = static_cast<const uint8_t *>(input);
	uint8_t *out = static_cast<uint8_t *>(output);

	/* \todo Can this be made more efficient? */
	for (unsigned int x = 0; x < width; x += 4) {
		*out++ = in[0];
		*out++ = (in[4] & 0x03) << 6 | in[1] >> 2;
		*out++ = (in[1] & 0x03) << 6 | (in[4] & 0x0c) << 2 | in[2] >> 4;
		*out++ = (in[2] & 0x0f) << 4 | (in[4] & 0x30) >> 2 | in[3] >> 6;
		*out++ = (in[3] & 0x3f) << 2 | (in[4] & 0xc0) >> 6;
		in += 5;
	}
}

void packScanlineSBGGR12P(void *output, const void *input, unsigned int width)
{
	const uint8_t *in = static_cast<const uint8_t *>(input);
	uint8_t *out = static_cast<uint8_t *>(output);

	/* \todo Can this be made more efficient? */
	for (unsigned int i = 0; i < width; i += 2) {
		*out++ = in[0];
		*out++ = (in[2] & 0x0f) << 4 | in[1] >> 4;
		*out++ = (in[1] & 0x0f) << 4 | in[2] >> 4;
		in += 3;
	}
}

void thumbScanlineSBGGRxxP(const FormatInfo &info, void *output,
			   const void *input, unsigned int width,
			   unsigned int stride)
{
	const uint8_t *in = static_cast<const uint8_t *>(input);
	uint8_t *out = static_cast<uint8_t *>(output);

	/* Number of bytes corresponding to 16 pixels. */
	unsigned int skip = info.bitsPerSample * 16 / 8;

	for (unsigned int x = 0; x < width; x++) {
		uint8_t value = (in[0] + in[1] + in[stride] + in[stride + 1]) >> 2;
		*out++ = value;
		*out++ = value;
		*out++ = value;
		in += skip;
	}
}

void packScanlineIPU3(void *output, const void *input, unsigned int width)
{
	const uint8_t *in = static_cast<const uint8_t *>(input);
	uint16_t *out = static_cast<uint16_t *>(output);

	/*
	 * Upscale the 10-bit format to 16-bit as it's not trivial to pack it
	 * as 10-bit without gaps.
	 *
	 * \todo Improve packing to keep the 10-bit sample size.
	 */
	unsigned int x = 0;
	while (true) {
		for (unsigned int i = 0; i < 6; i++) {
			*out++ = (in[1] & 0x03) << 14 | (in[0] & 0xff) << 6;
			if (++x >= width)
				return;

			*out++ = (in[2] & 0x0f) << 12 | (in[1] & 0xfc) << 4;
			if (++x >= width)
				return;

			*out++ = (in[3] & 0x3f) << 10 | (in[2] & 0xf0) << 2;
			if (++x >= width)
				return;

			*out++ = (in[4] & 0xff) <<  8 | (in[3] & 0xc0) << 0;
			if (++x >= width)
				return;

			in += 5;
		}

		*out++ = (in[1] & 0x03) << 14 | (in[0] & 0xff) << 6;
		if (++x >= width)
			return;

		in += 2;
	}
}

void thumbScanlineIPU3([[maybe_unused]] const FormatInfo &info, void *output,
		       const void *input, unsigned int width,
		       unsigned int stride)
{
	uint8_t *out = static_cast<uint8_t *>(output);

	for (unsigned int x = 0; x < width; x++) {
		unsigned int pixel = x * 16;
		unsigned int block = pixel / 25;
		unsigned int pixelInBlock = pixel - block * 25;

		/*
		 * If the pixel is the last in the block cheat a little and
		 * move one pixel backward to avoid reading between two blocks
		 * and having to deal with the padding bits.
		 */
		if (pixelInBlock == 24)
			pixelInBlock--;

		const uint8_t *in = static_cast<const uint8_t *>(input)
				  + block * 32 + (pixelInBlock / 4) * 5;

		uint16_t val1, val2, val3, val4;
		switch (pixelInBlock % 4) {
		case 0:
			val1 = (in[1] & 0x03) << 14 | (in[0] & 0xff) << 6;
			val2 = (in[2] & 0x0f) << 12 | (in[1] & 0xfc) << 4;
			val3 = (in[stride + 1] & 0x03) << 14 | (in[stride + 0] & 0xff) << 6;
			val4 = (in[stride + 2] & 0x0f) << 12 | (in[stride + 1] & 0xfc) << 4;
			break;
		case 1:
			val1 = (in[2] & 0x0f) << 12 | (in[1] & 0xfc) << 4;
			val2 = (in[3] & 0x3f) << 10 | (in[2] & 0xf0) << 2;
			val3 = (in[stride + 2] & 0x0f) << 12 | (in[stride + 1] & 0xfc) << 4;
			val4 = (in[stride + 3] & 0x3f) << 10 | (in[stride + 2] & 0xf0) << 2;
			break;
		case 2:
			val1 = (in[3] & 0x3f) << 10 | (in[2] & 0xf0) << 2;
			val2 = (in[4] & 0xff) <<  8 | (in[3] & 0xc0) << 0;
			val3 = (in[stride + 3] & 0x3f) << 10 | (in[stride + 2] & 0xf0) << 2;
			val4 = (in[stride + 4] & 0xff) <<  8 | (in[stride + 3] & 0xc0) << 0;
			break;
		case 3:
			val1 = (in[4] & 0xff) <<  8 | (in[3] & 0xc0) << 0;
			val2 = (in[6] & 0x03) << 14 | (in[5] & 0xff) << 6;
			val3 = (in[stride + 4] & 0xff) <<  8 | (in[stride + 3] & 0xc0) << 0;
			val4 = (in[stride + 6] & 0x03) << 14 | (in[stride + 5] & 0xff) << 6;
			break;
		}

		uint8_t value = (val1 + val2 + val3 + val4) >> 10;
		*out++ = value;
		*out++ = value;
		*out++ = value;
	}
}

static const std::map<PixelFormat, FormatInfo> formatInfo = {
	{ formats::SBGGR10_CSI2P, {
		.bitsPerSample = 10,
		.pattern = { CFAPatternBlue, CFAPatternGreen, CFAPatternGreen, CFAPatternRed },
		.packScanline = packScanlineSBGGR10P,
		.thumbScanline = thumbScanlineSBGGRxxP,
	} },
	{ formats::SGBRG10_CSI2P, {
		.bitsPerSample = 10,
		.pattern = { CFAPatternGreen, CFAPatternBlue, CFAPatternRed, CFAPatternGreen },
		.packScanline = packScanlineSBGGR10P,
		.thumbScanline = thumbScanlineSBGGRxxP,
	} },
	{ formats::SGRBG10_CSI2P, {
		.bitsPerSample = 10,
		.pattern = { CFAPatternGreen, CFAPatternRed, CFAPatternBlue, CFAPatternGreen },
		.packScanline = packScanlineSBGGR10P,
		.thumbScanline = thumbScanlineSBGGRxxP,
	} },
	{ formats::SRGGB10_CSI2P, {
		.bitsPerSample = 10,
		.pattern = { CFAPatternRed, CFAPatternGreen, CFAPatternGreen, CFAPatternBlue },
		.packScanline = packScanlineSBGGR10P,
		.thumbScanline = thumbScanlineSBGGRxxP,
	} },
	{ formats::SBGGR12_CSI2P, {
		.bitsPerSample = 12,
		.pattern = { CFAPatternBlue, CFAPatternGreen, CFAPatternGreen, CFAPatternRed },
		.packScanline = packScanlineSBGGR12P,
		.thumbScanline = thumbScanlineSBGGRxxP,
	} },
	{ formats::SGBRG12_CSI2P, {
		.bitsPerSample = 12,
		.pattern = { CFAPatternGreen, CFAPatternBlue, CFAPatternRed, CFAPatternGreen },
		.packScanline = packScanlineSBGGR12P,
		.thumbScanline = thumbScanlineSBGGRxxP,
	} },
	{ formats::SGRBG12_CSI2P, {
		.bitsPerSample = 12,
		.pattern = { CFAPatternGreen, CFAPatternRed, CFAPatternBlue, CFAPatternGreen },
		.packScanline = packScanlineSBGGR12P,
		.thumbScanline = thumbScanlineSBGGRxxP,
	} },
	{ formats::SRGGB12_CSI2P, {
		.bitsPerSample = 12,
		.pattern = { CFAPatternRed, CFAPatternGreen, CFAPatternGreen, CFAPatternBlue },
		.packScanline = packScanlineSBGGR12P,
		.thumbScanline = thumbScanlineSBGGRxxP,
	} },
	{ formats::SBGGR10_IPU3, {
		.bitsPerSample = 16,
		.pattern = { CFAPatternBlue, CFAPatternGreen, CFAPatternGreen, CFAPatternRed },
		.packScanline = packScanlineIPU3,
		.thumbScanline = thumbScanlineIPU3,
	} },
	{ formats::SGBRG10_IPU3, {
		.bitsPerSample = 16,
		.pattern = { CFAPatternGreen, CFAPatternBlue, CFAPatternRed, CFAPatternGreen },
		.packScanline = packScanlineIPU3,
		.thumbScanline = thumbScanlineIPU3,
	} },
	{ formats::SGRBG10_IPU3, {
		.bitsPerSample = 16,
		.pattern = { CFAPatternGreen, CFAPatternRed, CFAPatternBlue, CFAPatternGreen },
		.packScanline = packScanlineIPU3,
		.thumbScanline = thumbScanlineIPU3,
	} },
	{ formats::SRGGB10_IPU3, {
		.bitsPerSample = 16,
		.pattern = { CFAPatternRed, CFAPatternGreen, CFAPatternGreen, CFAPatternBlue },
		.packScanline = packScanlineIPU3,
		.thumbScanline = thumbScanlineIPU3,
	} },
};

int DNGWriter::write(const char *filename, const Camera *camera,
		     const StreamConfiguration &config,
		     const ControlList &metadata,
		     [[maybe_unused]] const FrameBuffer *buffer,
		     const void *data)
{
	const ControlList &cameraProperties = camera->properties();

	const auto it = formatInfo.find(config.pixelFormat);
	if (it == formatInfo.cend()) {
		std::cerr << "Unsupported pixel format" << std::endl;
		return -EINVAL;
	}
	const FormatInfo *info = &it->second;

	TIFF *tif = TIFFOpen(filename, "w");
	if (!tif) {
		std::cerr << "Failed to open tiff file" << std::endl;
		return -EINVAL;
	}

	/*
	 * Scanline buffer, has to be large enough to store both a RAW scanline
	 * or a thumbnail scanline. The latter will always be much smaller than
	 * the former as we downscale by 16 in both directions.
	 */
	uint8_t scanline[(config.size.width * info->bitsPerSample + 7) / 8];

	toff_t rawIFDOffset = 0;
	toff_t exifIFDOffset = 0;

	/*
	 * Start with a thumbnail in IFD 0 for compatibility with TIFF baseline
	 * readers, as required by the TIFF/EP specification. Tags that apply to
	 * the whole file are stored here.
	 */
	const uint8_t version[] = { 1, 2, 0, 0 };

	TIFFSetField(tif, TIFFTAG_DNGVERSION, version);
	TIFFSetField(tif, TIFFTAG_DNGBACKWARDVERSION, version);
	TIFFSetField(tif, TIFFTAG_FILLORDER, FILLORDER_MSB2LSB);
	TIFFSetField(tif, TIFFTAG_MAKE, "libcamera");

	const auto &model = cameraProperties.get(properties::Model);
	if (model) {
		TIFFSetField(tif, TIFFTAG_MODEL, model->c_str());
		/* \todo set TIFFTAG_UNIQUECAMERAMODEL. */
	}

	TIFFSetField(tif, TIFFTAG_SOFTWARE, "qcam");
	TIFFSetField(tif, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT);

	/*
	 * Thumbnail-specific tags. The thumbnail is stored as an RGB image
	 * with 1/16 of the raw image resolution. Greyscale would save space,
	 * but doesn't seem well supported by RawTherapee.
	 */
	TIFFSetField(tif, TIFFTAG_SUBFILETYPE, FILETYPE_REDUCEDIMAGE);
	TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, config.size.width / 16);
	TIFFSetField(tif, TIFFTAG_IMAGELENGTH, config.size.height / 16);
	TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, 8);
	TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE);
	TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB);
	TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 3);
	TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
	TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT);

	/*
	 * Fill in some reasonable colour information in the DNG. We supply
	 * the "neutral" colour values which determine the white balance, and the
	 * "ColorMatrix1" which converts XYZ to (un-white-balanced) camera RGB.
	 * Note that this is not a "proper" colour calibration for the DNG,
	 * nonetheless, many tools should be able to render the colours better.
	 */
	float neutral[3] = { 1, 1, 1 };
	Matrix3d wbGain = Matrix3d::identity();
	/* From http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html */
	const Matrix3d rgb2xyz(0.4124564, 0.3575761, 0.1804375,
			       0.2126729, 0.7151522, 0.0721750,
			       0.0193339, 0.1191920, 0.9503041);
	Matrix3d ccm = Matrix3d::identity();
	/*
	 * Pick a reasonable number eps to protect against singularities. It
	 * should be comfortably larger than the point at which we run into
	 * numerical trouble, yet smaller than any plausible gain that we might
	 * apply to a colour, either explicitly or as part of the colour matrix.
	 */
	const double eps = 1e-2;

	const auto &colourGains = metadata.get(controls::ColourGains);
	if (colourGains) {
		if ((*colourGains)[0] > eps && (*colourGains)[1] > eps) {
			wbGain = Matrix3d::diag((*colourGains)[0], 1, (*colourGains)[1]);
			neutral[0] = 1.0 / (*colourGains)[0]; /* red */
			neutral[2] = 1.0 / (*colourGains)[1]; /* blue */
		}
	}

	const auto &ccmControl = metadata.get(controls::ColourCorrectionMatrix);
	if (ccmControl) {
		Matrix3d ccmSupplied(*ccmControl);
		if (ccmSupplied.determinant() > eps)
			ccm = ccmSupplied;
	}

	/*
	 * rgb2xyz is known to be invertible, and we've ensured above that both
	 * the ccm and wbGain matrices are non-singular, so the product of all
	 * three is guaranteed to be invertible too.
	 */
	Matrix3d colorMatrix1 = (rgb2xyz * ccm * wbGain).inverse();

	TIFFSetField(tif, TIFFTAG_COLORMATRIX1, 9, colorMatrix1.m);
	TIFFSetField(tif, TIFFTAG_ASSHOTNEUTRAL, 3, neutral);

	/*
	 * Reserve space for the SubIFD and ExifIFD tags, pointing to the IFD
	 * for the raw image and EXIF data respectively. The real offsets will
	 * be set later.
	 */
	TIFFSetField(tif, TIFFTAG_SUBIFD, 1, &rawIFDOffset);
	TIFFSetField(tif, TIFFTAG_EXIFIFD, exifIFDOffset);

	/* Write the thumbnail. */
	const uint8_t *row = static_cast<const uint8_t *>(data);
	for (unsigned int y = 0; y < config.size.height / 16; y++) {
		info->thumbScanline(*info, &scanline, row,
				    config.size.width / 16, config.stride);

		if (TIFFWriteScanline(tif, &scanline, y, 0) != 1) {
			std::cerr << "Failed to write thumbnail scanline"
				  << std::endl;
			TIFFClose(tif);
			return -EINVAL;
		}

		row += config.stride * 16;
	}

	TIFFWriteDirectory(tif);

	/* Create a new IFD for the RAW image. */
	const uint16_t cfaRepeatPatternDim[] = { 2, 2 };
	const uint8_t cfaPlaneColor[] = {
		CFAPatternRed,
		CFAPatternGreen,
		CFAPatternBlue
	};

	TIFFSetField(tif, TIFFTAG_SUBFILETYPE, 0);
	TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, config.size.width);
	TIFFSetField(tif, TIFFTAG_IMAGELENGTH, config.size.height);
	TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, info->bitsPerSample);
	TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE);
	TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_CFA);
	TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 1);
	TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
	TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT);
	TIFFSetField(tif, TIFFTAG_CFAREPEATPATTERNDIM, cfaRepeatPatternDim);
	TIFFSetField(tif, TIFFTAG_CFAPATTERN, info->pattern);
	TIFFSetField(tif, TIFFTAG_CFAPLANECOLOR, 3, cfaPlaneColor);
	TIFFSetField(tif, TIFFTAG_CFALAYOUT, 1);

	const uint16_t blackLevelRepeatDim[] = { 2, 2 };
	float blackLevel[] = { 0.0f, 0.0f, 0.0f, 0.0f };
	uint32_t whiteLevel = (1 << info->bitsPerSample) - 1;

	const auto &blackLevels = metadata.get(controls::SensorBlackLevels);
	if (blackLevels) {
		Span<const int32_t> levels = *blackLevels;

		/*
		 * The black levels control is specified in R, Gr, Gb, B order.
		 * Map it to the TIFF tag that is specified in CFA pattern
		 * order.
		 */
		unsigned int green = (info->pattern[0] == CFAPatternRed ||
				      info->pattern[1] == CFAPatternRed)
				   ? 0 : 1;

		for (unsigned int i = 0; i < 4; ++i) {
			unsigned int level;

			switch (info->pattern[i]) {
			case CFAPatternRed:
				level = levels[0];
				break;
			case CFAPatternGreen:
				level = levels[green + 1];
				green = (green + 1) % 2;
				break;
			case CFAPatternBlue:
			default:
				level = levels[3];
				break;
			}

			/* Map the 16-bit value to the bits per sample range. */
			blackLevel[i] = level >> (16 - info->bitsPerSample);
		}
	}

	TIFFSetField(tif, TIFFTAG_BLACKLEVELREPEATDIM, &blackLevelRepeatDim);
	TIFFSetField(tif, TIFFTAG_BLACKLEVEL, 4, &blackLevel);
	TIFFSetField(tif, TIFFTAG_WHITELEVEL, 1, &whiteLevel);

	/* Write RAW content. */
	row = static_cast<const uint8_t *>(data);
	for (unsigned int y = 0; y < config.size.height; y++) {
		info->packScanline(&scanline, row, config.size.width);

		if (TIFFWriteScanline(tif, &scanline, y, 0) != 1) {
			std::cerr << "Failed to write RAW scanline"
				  << std::endl;
			TIFFClose(tif);
			return -EINVAL;
		}

		row += config.stride;
	}

	/* Checkpoint the IFD to retrieve its offset, and write it out. */
	TIFFCheckpointDirectory(tif);
	rawIFDOffset = TIFFCurrentDirOffset(tif);
	TIFFWriteDirectory(tif);

	/* Create a new IFD for the EXIF data and fill it. */
	TIFFCreateEXIFDirectory(tif);

	/* Store creation time. */
	time_t rawtime;
	struct tm *timeinfo;
	char strTime[20];

	time(&rawtime);
	timeinfo = localtime(&rawtime);
	strftime(strTime, 20, "%Y:%m:%d %H:%M:%S", timeinfo);

	/*
	 * \todo Handle timezone information by setting OffsetTimeOriginal and
	 * OffsetTimeDigitized once libtiff catches up to the specification and
	 * has EXIFTAG_ defines to handle them.
	 */
	TIFFSetField(tif, EXIFTAG_DATETIMEORIGINAL, strTime);
	TIFFSetField(tif, EXIFTAG_DATETIMEDIGITIZED, strTime);

	const auto &analogGain = metadata.get(controls::AnalogueGain);
	if (analogGain) {
		uint16_t iso = std::min(std::max(*analogGain * 100, 0.0f), 65535.0f);
		TIFFSetField(tif, EXIFTAG_ISOSPEEDRATINGS, 1, &iso);
	}

	const auto &exposureTime = metadata.get(controls::ExposureTime);
	if (exposureTime)
		TIFFSetField(tif, EXIFTAG_EXPOSURETIME, *exposureTime / 1e6);

	TIFFWriteCustomDirectory(tif, &exifIFDOffset);

	/* Update the IFD offsets and close the file. */
	TIFFSetDirectory(tif, 0);
	TIFFSetField(tif, TIFFTAG_SUBIFD, 1, &rawIFDOffset);
	TIFFSetField(tif, TIFFTAG_EXIFIFD, exifIFDOffset);
	TIFFWriteDirectory(tif);

	TIFFClose(tif);

	return 0;
}