/*
    This file is part of darktable,
    Copyright (C) 2011-2026 darktable developers.

    darktable is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    darktable is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with darktable.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "common/darktable.h"
#include "imageio/imageio_dng.h"
#include "imageio/imageio_jpeg.h"
#include "common/colorspaces_inline_conversions.h"
#include "common/exif.h"
#include "common/image.h"
#include "develop/imageop_math.h"

#include <glib.h>
#include <glib/gstdio.h>
#include <inttypes.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <tiffio.h>

#ifdef _WIN32
#include <wchar.h>
#endif

// fallback for libtiff < 4.6
#ifndef TIFFTAG_PREVIEWCOLORSPACE
#define TIFFTAG_PREVIEWCOLORSPACE 50970
#endif

// DNG uses SRATIONAL / RATIONAL for matrix and WB tags. libtiff accepts
// these as float/double arrays and handles the conversion; we just pass
// the values as double

// libtiff error/warning handlers — replacing whatever else in the
// process may have installed (e.g. ImageMagick's, which crashes on
// NULL exception context). these just log to stderr and never abort
static void _dt_dng_tiff_warning(const char *module,
                                 const char *fmt, va_list ap)
{
  if(darktable.unmuted & DT_DEBUG_IMAGEIO)
  {
    fprintf(stderr, "%11.4f [imageio_dng] warning: %s: ",
            dt_get_wtime() - darktable.start_wtime,
            module ? module : "(none)");
    vfprintf(stderr, fmt, ap);
    fprintf(stderr, "\n");
  }
}

static void _dt_dng_tiff_error(const char *module,
                               const char *fmt, va_list ap)
{
  fprintf(stderr, "%11.4f [imageio_dng] error: %s: ",
          dt_get_wtime() - darktable.start_wtime,
          module ? module : "(none)");
  vfprintf(stderr, fmt, ap);
  fprintf(stderr, "\n");
}

static void _install_dng_tiff_handlers(void)
{
  TIFFSetWarningHandler(_dt_dng_tiff_warning);
  TIFFSetErrorHandler(_dt_dng_tiff_error);
}

// register DNG tags that some libtiff builds lack or drop between IFDs.
// PreviewColorSpace is missing from the built-in table; CFA pattern
// tags survive IFD0 but vanish in SubIFD0 on e.g. the libtiff that
// ships with Linux Mint, breaking rawspeed re-import
static void _register_extra_dng_fields(TIFF *tif)
{
  static const TIFFFieldInfo extra[] = {
    { TIFFTAG_PREVIEWCOLORSPACE, 1, 1, TIFF_LONG, FIELD_CUSTOM,
      TRUE, FALSE, (char *)"PreviewColorSpace" },
    { TIFFTAG_CFAREPEATPATTERNDIM, 2, 2, TIFF_SHORT, FIELD_CUSTOM,
      TRUE, FALSE, (char *)"CFARepeatPatternDim" },
    { TIFFTAG_CFAPATTERN, -1, -1, TIFF_BYTE, FIELD_CUSTOM,
      TRUE, TRUE, (char *)"CFAPattern" },
  };
  TIFFMergeFieldInfo(tif, extra, sizeof(extra) / sizeof(extra[0]));
}

// map the dcraw 2x2 CFA filters word to 4 single-byte channel indices
// for the DNG CFAPattern tag: 0=R, 1=G, 2=B, following DNG spec §A.3.1
static void _cfa_bytes_from_filters(uint32_t filters, uint8_t out[4])
{
  out[0] = FC(0, 0, filters);
  out[1] = FC(0, 1, filters);
  out[2] = FC(1, 0, filters);
  out[3] = FC(1, 1, filters);
}

// shared DNG metadata block: written on whichever IFD readers consult
// first for camera/colour information. for single-IFD layouts that's
// the raw IFD; for the canonical preview-leading layout (IFD0 = JPEG
// preview, SubIFD0 = raw) it's IFD0
static void _set_dng_shared_metadata(TIFF *tif, const dt_image_t *img)
{
  TIFFSetField(tif, TIFFTAG_XRESOLUTION, 300.0);
  TIFFSetField(tif, TIFFTAG_YRESOLUTION, 300.0);
  TIFFSetField(tif, TIFFTAG_RESOLUTIONUNIT, RESUNIT_INCH);

  gchar *software = g_strdup_printf("darktable %s", darktable_package_version);
  TIFFSetField(tif, TIFFTAG_SOFTWARE, software);
  g_free(software);

  if(img->camera_maker[0])
    TIFFSetField(tif, TIFFTAG_MAKE, img->camera_maker);
  if(img->camera_model[0])
    TIFFSetField(tif, TIFFTAG_MODEL, img->camera_model);
  if(img->camera_makermodel[0])
    TIFFSetField(tif, TIFFTAG_UNIQUECAMERAMODEL, img->camera_makermodel);

  // OriginalRawFileName: UTF-8 bytes, not zero-terminated ASCII
  if(img->filename[0])
  {
    const size_t fn_len = strlen(img->filename);
    TIFFSetField(tif, TIFFTAG_ORIGINALRAWFILENAME,
                 (uint32_t)fn_len, img->filename);
  }

  const uint8_t dng_version[4]  = { 1, 4, 0, 0 };
  const uint8_t dng_backward[4] = { 1, 2, 0, 0 };
  TIFFSetField(tif, TIFFTAG_DNGVERSION, dng_version);
  TIFFSetField(tif, TIFFTAG_DNGBACKWARDVERSION, dng_backward);

  TIFFSetField(tif, TIFFTAG_BASELINEEXPOSURE, 0.0f);

  // AsShotNeutral: inverse of wb_coeffs, normalized so max=1. omit
  // entirely when missing so the reader derives a default from
  // CalibrationIlluminant1 + ColorMatrix1 instead of a fake daylight
  if(img->wb_coeffs[0] > 0.0f
     && img->wb_coeffs[1] > 0.0f
     && img->wb_coeffs[2] > 0.0f)
  {
    float neutral[3];
    for(int i = 0; i < 3; i++) neutral[i] = 1.0f / img->wb_coeffs[i];
    const float m = fmaxf(neutral[0], fmaxf(neutral[1], neutral[2]));
    if(m > 0.0f) for(int i = 0; i < 3; i++) neutral[i] /= m;
    TIFFSetField(tif, TIFFTAG_ASSHOTNEUTRAL, 3, neutral);
  }

  // ColorMatrix1 (XYZ D65 -> cameraRGB, 3x3, row-major [camRGB][XYZ]).
  // fall back to XYZ-D65->sRGB when source has no matrix so the DNG is
  // still renderable; mirrors dt_imageio_dng_write_float
  float color_matrix[9];
  float non_zero = 0.0f;
  for(int k = 0; k < 3; k++)
    for(int i = 0; i < 3; i++)
      non_zero += fabsf(img->adobe_XYZ_to_CAM[k][i]);
  if(non_zero > 0.0f)
  {
    for(int k = 0; k < 3; k++)
      for(int i = 0; i < 3; i++)
        color_matrix[k * 3 + i] = img->adobe_XYZ_to_CAM[k][i];
  }
  else
  {
    memcpy(color_matrix, xyz_to_srgb_d65, sizeof(color_matrix));
  }
  TIFFSetField(tif, TIFFTAG_COLORMATRIX1, 9, color_matrix);
  // CalibrationIlluminant1 = D65 (21). required alongside ColorMatrix1
  // — without it strict readers (and older dt) ignore the matrix
  TIFFSetField(tif, TIFFTAG_CALIBRATIONILLUMINANT1, (uint16_t)21);
}

// Adobe DNG IFD0 thumbnail convention: ~256 px long-edge JPEG.
// decode supplied preview, downsample, re-encode. returns g_malloc'd
// bytes (caller frees) or NULL on failure
#define DNG_THUMB_MAX_DIM 256
#define DNG_THUMB_JPEG_QUALITY 85
static uint8_t *_make_thumb_jpeg(const uint8_t *src_jpeg,
                                 const int src_len,
                                 int *out_w,
                                 int *out_h,
                                 int *out_len)
{
  dt_imageio_jpeg_t jpg;
  if(dt_imageio_jpeg_decompress_header(src_jpeg, (size_t)src_len, &jpg))
    return NULL;
  if(jpg.width <= 0 || jpg.height <= 0) return NULL;

  uint8_t *rgbx = dt_alloc_align_uint8((size_t)4 * jpg.width * jpg.height);
  if(!rgbx) return NULL;
  if(dt_imageio_jpeg_decompress(&jpg, rgbx))
  {
    dt_free_align(rgbx);
    return NULL;
  }

  const int sw = jpg.width, sh = jpg.height;
  const int long_edge = sw > sh ? sw : sh;
  const int dw = (long_edge <= DNG_THUMB_MAX_DIM)
    ? sw : (sw * DNG_THUMB_MAX_DIM + long_edge / 2) / long_edge;
  const int dh = (long_edge <= DNG_THUMB_MAX_DIM)
    ? sh : (sh * DNG_THUMB_MAX_DIM + long_edge / 2) / long_edge;

  uint8_t *small = dt_alloc_align_uint8((size_t)4 * dw * dh);
  if(!small) { dt_free_align(rgbx); return NULL; }

  for(int y = 0; y < dh; y++)
  {
    const int sy0 = y * sh / dh;
    const int sy1 = (y + 1) * sh / dh;
    for(int x = 0; x < dw; x++)
    {
      const int sx0 = x * sw / dw;
      const int sx1 = (x + 1) * sw / dw;
      uint32_t r = 0, g = 0, b = 0, n = 0;
      for(int sy = sy0; sy < sy1; sy++)
        for(int sx = sx0; sx < sx1; sx++)
        {
          const uint8_t *p = rgbx + 4 * ((size_t)sy * sw + sx);
          r += p[0]; g += p[1]; b += p[2]; n++;
        }
      if(n == 0) n = 1;
      uint8_t *dst = small + 4 * ((size_t)y * dw + x);
      dst[0] = (uint8_t)(r / n);
      dst[1] = (uint8_t)(g / n);
      dst[2] = (uint8_t)(b / n);
      dst[3] = 0;
    }
  }
  dt_free_align(rgbx);

  const size_t max_out = (size_t)4 * dw * dh + 1024;
  uint8_t *jpeg = g_try_malloc(max_out);
  if(!jpeg) { dt_free_align(small); return NULL; }
  const int written = dt_imageio_jpeg_compress(small, jpeg, dw, dh,
                                               DNG_THUMB_JPEG_QUALITY);
  dt_free_align(small);
  if(written <= 0 || written > (int)max_out)
  {
    g_free(jpeg);
    return NULL;
  }
  *out_w = dw;
  *out_h = dh;
  *out_len = written;
  return jpeg;
}

// write canonical Adobe-layout IFD0: JPEG thumbnail + DNG metadata +
// SubIFD pointer table (n_subifds = 1 raw-only, or 2 with full preview)
static int _write_thumb_ifd0(TIFF *tif,
                             const dt_image_t *img,
                             const dt_imageio_dng_preview_t *p,
                             const int n_subifds)
{
  TIFFSetField(tif, TIFFTAG_SUBFILETYPE, FILETYPE_REDUCEDIMAGE);
  TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, (uint32_t)p->width);
  TIFFSetField(tif, TIFFTAG_IMAGELENGTH, (uint32_t)p->height);
  TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, (uint16_t)8);
  TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, (uint16_t)3);
  TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
  TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_YCBCR);
  TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_JPEG);
  TIFFSetField(tif, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT);
  TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, (uint32_t)p->height);
  TIFFSetField(tif, TIFFTAG_PREVIEWCOLORSPACE, (uint32_t)2);

  _set_dng_shared_metadata(tif, img);

  toff_t sub_offsets[2] = { 0, 0 };
  TIFFSetField(tif, TIFFTAG_SUBIFD, (uint16_t)n_subifds, sub_offsets);

  if(TIFFWriteRawStrip(tif, 0, (void *)p->data, (tmsize_t)p->len) < 0)
  {
    dt_print(DT_DEBUG_ALWAYS,
             "[imageio_dng] TIFFWriteRawStrip failed for JPEG thumbnail "
             "(%d bytes, %dx%d)", p->len, p->width, p->height);
    return 1;
  }
  if(!TIFFWriteDirectory(tif))
  {
    dt_print(DT_DEBUG_ALWAYS,
             "[imageio_dng] TIFFWriteDirectory failed for JPEG thumbnail IFD0");
    return 1;
  }
  return 0;
}

// write SubIFD1: full-res JPEG preview, no DNG tags (they live on IFD0)
static int _write_full_preview_subifd(TIFF *tif,
                                      const dt_imageio_dng_preview_t *p)
{
  TIFFSetField(tif, TIFFTAG_SUBFILETYPE, FILETYPE_REDUCEDIMAGE);
  TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, (uint32_t)p->width);
  TIFFSetField(tif, TIFFTAG_IMAGELENGTH, (uint32_t)p->height);
  TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, (uint16_t)8);
  TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, (uint16_t)3);
  TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
  TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_YCBCR);
  TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_JPEG);
  TIFFSetField(tif, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT);
  TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, (uint32_t)p->height);
  TIFFSetField(tif, TIFFTAG_PREVIEWCOLORSPACE, (uint32_t)2);

  if(TIFFWriteRawStrip(tif, 0, (void *)p->data, (tmsize_t)p->len) < 0)
  {
    dt_print(DT_DEBUG_ALWAYS,
             "[imageio_dng] TIFFWriteRawStrip failed for full preview "
             "(%d bytes, %dx%d)", p->len, p->width, p->height);
    return 1;
  }
  if(!TIFFWriteDirectory(tif))
  {
    dt_print(DT_DEBUG_ALWAYS,
             "[imageio_dng] TIFFWriteDirectory failed for full preview SubIFD");
    return 1;
  }
  return 0;
}

int dt_imageio_dng_write_cfa_bayer(const char *filename,
                                   const uint16_t *cfa,
                                   int width,
                                   int height,
                                   const dt_image_t *img,
                                   const void *exif_blob,
                                   int exif_len,
                                   const dt_imageio_dng_preview_t *preview)
{
  if(!filename || !cfa || !img || width <= 0 || height <= 0)
    return 1;

  _install_dng_tiff_handlers();

#ifdef _WIN32
  wchar_t *wfilename = g_utf8_to_utf16(filename, -1, NULL, NULL, NULL);
  TIFF *tif = TIFFOpenW(wfilename, "wl");
  g_free(wfilename);
#else
  TIFF *tif = TIFFOpen(filename, "wl");
#endif
  if(!tif) return 1;
  _register_extra_dng_fields(tif);

  // canonical Adobe layout: IFD0=thumb, SubIFD0=raw, SubIFD1=full preview.
  // on thumb generation failure fall back to preview-as-IFD0 (old behavior)
  const gboolean canonical = (preview && preview->data && preview->len > 0
                              && preview->width > 0 && preview->height > 0);
  uint8_t *thumb_jpeg = NULL;
  int thumb_w = 0, thumb_h = 0, thumb_len = 0;
  if(canonical)
    thumb_jpeg = _make_thumb_jpeg(preview->data, preview->len,
                                  &thumb_w, &thumb_h, &thumb_len);
  const gboolean have_thumb = (thumb_jpeg != NULL);

  if(canonical)
  {
    dt_imageio_dng_preview_t ifd0;
    if(have_thumb)
      ifd0 = (dt_imageio_dng_preview_t){
        .data = thumb_jpeg, .len = thumb_len,
        .width = thumb_w, .height = thumb_h };
    else
      ifd0 = *preview;

    if(_write_thumb_ifd0(tif, img, &ifd0, have_thumb ? 2 : 1) != 0)
    {
      dt_print(DT_DEBUG_ALWAYS,
               "[imageio_dng] write_cfa_bayer: thumbnail IFD0 failed, aborting");
      g_free(thumb_jpeg);
      TIFFClose(tif);
      g_unlink(filename);
      return 1;
    }
    // some libtiff builds drop merged field info on TIFFCreateDirectory,
    // so re-register or CFA tags are rejected as unknown on the SubIFD
    TIFFCreateDirectory(tif);
    _register_extra_dng_fields(tif);
  }

  // raw payload IFD: single IFD when no preview, otherwise SubIFD0
  TIFFSetField(tif, TIFFTAG_SUBFILETYPE, 0);
  TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, (uint32_t)width);
  TIFFSetField(tif, TIFFTAG_IMAGELENGTH, (uint32_t)height);
  TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, (uint16_t)16);
  TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, (uint16_t)1);
  TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
  TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_CFA);
  TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT);
  TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE);
  TIFFSetField(tif, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT);
  TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, TIFFDefaultStripSize(tif, 0));
  // shared metadata only on single-IFD layout — canonical has it on IFD0
  if(!canonical)
    _set_dng_shared_metadata(tif, img);

  const int crop_x = (img->crop_x > 0) ? img->crop_x : 0;
  const int crop_y = (img->crop_y > 0) ? img->crop_y : 0;
  const int vis_w  = (img->p_width  > 0 && img->p_width  <= width  - crop_x)
                     ? img->p_width  : (width  - crop_x);
  const int vis_h  = (img->p_height > 0 && img->p_height <= height - crop_y)
                     ? img->p_height : (height - crop_y);

  // CFAPattern and BlackLevel are indexed from ActiveArea[0,0], so
  // rotate per-site arrays when ActiveArea starts on an odd row/col
  const int dy = crop_y & 1;
  const int dx = crop_x & 1;

  const uint16_t cfa_repeat_dim[2] = { 2, 2 };
  TIFFSetField(tif, TIFFTAG_CFAREPEATPATTERNDIM, cfa_repeat_dim);

  uint8_t cfa_pattern[4];
  {
    uint8_t native[4];
    _cfa_bytes_from_filters(img->buf_dsc.filters, native);
    for(int yy = 0; yy < 2; yy++)
      for(int xx = 0; xx < 2; xx++)
        cfa_pattern[yy * 2 + xx] = native[((yy + dy) & 1) * 2 + ((xx + dx) & 1)];
  }
  TIFFSetField(tif, TIFFTAG_CFAPATTERN, 4, cfa_pattern);

  const uint8_t cfa_plane_color[3] = { 0, 1, 2 };   // R, G, B
  TIFFSetField(tif, TIFFTAG_CFAPLANECOLOR, 3, cfa_plane_color);
  TIFFSetField(tif, TIFFTAG_CFALAYOUT, (uint16_t)1); // rectangular

  const uint16_t bl_repeat_dim[2] = { 2, 2 };
  TIFFSetField(tif, TIFFTAG_BLACKLEVELREPEATDIM, bl_repeat_dim);

  float black_level[4];
  const gboolean have_separate
    = (img->raw_black_level_separate[0] != 0
       || img->raw_black_level_separate[1] != 0
       || img->raw_black_level_separate[2] != 0
       || img->raw_black_level_separate[3] != 0);
  if(have_separate)
  {
    for(int yy = 0; yy < 2; yy++)
      for(int xx = 0; xx < 2; xx++)
        black_level[yy * 2 + xx] =
          (float)img->raw_black_level_separate[((yy + dy) & 1) * 2 + ((xx + dx) & 1)];
  }
  else
  {
    for(int i = 0; i < 4; i++)
      black_level[i] = (float)img->raw_black_level;
  }
  TIFFSetField(tif, TIFFTAG_BLACKLEVEL, 4, black_level);

  const uint32_t white = img->raw_white_point
    ? img->raw_white_point : 65535u;
  TIFFSetField(tif, TIFFTAG_WHITELEVEL, 1, &white);

  // advertise the visible region inside the full raw buffer; without
  // these tags the importer renders the optical-black margins too

  const uint32_t active_area[4] = {
    (uint32_t)crop_y, (uint32_t)crop_x,
    (uint32_t)(crop_y + vis_h), (uint32_t)(crop_x + vis_w),
  };
  const float default_scale[2] = { 1.0f, 1.0f };
  const float default_crop_origin[2] = { 0.0f, 0.0f };
  const float default_crop_size[2] = { (float)vis_w, (float)vis_h };
  TIFFSetField(tif, TIFFTAG_ACTIVEAREA, active_area);
  TIFFSetField(tif, TIFFTAG_DEFAULTSCALE, default_scale);
  TIFFSetField(tif, TIFFTAG_DEFAULTCROPORIGIN, default_crop_origin);
  TIFFSetField(tif, TIFFTAG_DEFAULTCROPSIZE, default_crop_size);

  // scanline write
  int res = 0;
  for(int y = 0; y < height && res == 0; y++)
  {
    const uint16_t *row = cfa + (size_t)y * width;
    if(TIFFWriteScanline(tif, (void *)row, y, 0) < 0)
      res = 1;
  }

  // SubIFD1 = full-res preview when we have both thumb and original
  if(res == 0 && have_thumb)
  {
    if(!TIFFWriteDirectory(tif))
    {
      dt_print(DT_DEBUG_ALWAYS,
               "[imageio_dng] TIFFWriteDirectory failed closing raw SubIFD0");
      res = 1;
    }
    else
    {
      TIFFCreateDirectory(tif);
      _register_extra_dng_fields(tif);
      if(_write_full_preview_subifd(tif, preview) != 0) res = 1;
    }
  }

  TIFFClose(tif);
  g_free(thumb_jpeg);

  // embed source EXIF (datetime, ISO, shutter, etc.)
  // dt_exif_write_blob takes a non-const pointer; we don't modify it
  if(res == 0 && exif_blob && exif_len > 0)
    dt_exif_write_blob((uint8_t *)exif_blob, (uint32_t)exif_len,
                       filename, 0);

  if(res != 0)
    g_unlink(filename);

  return res;
}

int dt_imageio_dng_write_linear(const char *filename,
                                const float *rgb,
                                int width,
                                int height,
                                const dt_image_t *img,
                                const void *exif_blob,
                                int exif_len,
                                const dt_imageio_dng_preview_t *preview)
{
  if(!filename || !rgb || !img || width <= 0 || height <= 0)
    return 1;

  _install_dng_tiff_handlers();

#ifdef _WIN32
  wchar_t *wfilename = g_utf8_to_utf16(filename, -1, NULL, NULL, NULL);
  TIFF *tif = TIFFOpenW(wfilename, "wl");
  g_free(wfilename);
#else
  TIFF *tif = TIFFOpen(filename, "wl");
#endif
  if(!tif) return 1;
  _register_extra_dng_fields(tif);

  // canonical layout when a preview is provided (see write_cfa_bayer)
  const gboolean canonical = (preview && preview->data && preview->len > 0
                              && preview->width > 0 && preview->height > 0);
  uint8_t *thumb_jpeg = NULL;
  int thumb_w = 0, thumb_h = 0, thumb_len = 0;
  if(canonical)
    thumb_jpeg = _make_thumb_jpeg(preview->data, preview->len,
                                  &thumb_w, &thumb_h, &thumb_len);
  const gboolean have_thumb = (thumb_jpeg != NULL);

  if(canonical)
  {
    dt_imageio_dng_preview_t ifd0;
    if(have_thumb)
      ifd0 = (dt_imageio_dng_preview_t){
        .data = thumb_jpeg, .len = thumb_len,
        .width = thumb_w, .height = thumb_h };
    else
      ifd0 = *preview;

    if(_write_thumb_ifd0(tif, img, &ifd0, have_thumb ? 2 : 1) != 0)
    {
      dt_print(DT_DEBUG_ALWAYS,
               "[imageio_dng] write_linear: thumbnail IFD0 failed, aborting");
      g_free(thumb_jpeg);
      TIFFClose(tif);
      g_unlink(filename);
      return 1;
    }
    // see comment in write_cfa_bayer
    TIFFCreateDirectory(tif);
    _register_extra_dng_fields(tif);
  }

  // baseline TIFF tags, 3 samples per pixel (demosaicked)
  TIFFSetField(tif, TIFFTAG_SUBFILETYPE, 0);
  TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, (uint32_t)width);
  TIFFSetField(tif, TIFFTAG_IMAGELENGTH, (uint32_t)height);
  TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, (uint16_t)16);
  TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, (uint16_t)3);
  TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
  TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, 34892);  // LinearRaw
  TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT);
  TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE);
  TIFFSetField(tif, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT);
  TIFFSetField(tif, TIFFTAG_ROWSPERSTRIP, TIFFDefaultStripSize(tif, 0));
  if(!canonical)
    _set_dng_shared_metadata(tif, img);

  // NO CFA tags: this is demosaicked data.
  //     encode as normalized: BlackLevel=0, WhiteLevel=65535. the
  //     pixel data is already un-WB'd camRGB in [0, 1] range (the
  //     raw_restore_linear pipeline does matrix + un-boost + un-WB
  //     before handing off). the consumer applies WB via
  //     AsShotNeutral, reads uint16 as [0, 65535] and normalizes to
  //     [0, 1] via black/white
  const uint32_t white_norm = 65535u;
  const float black3[3] = { 0.0f, 0.0f, 0.0f };
  TIFFSetField(tif, TIFFTAG_BLACKLEVEL, 3, black3);
  TIFFSetField(tif, TIFFTAG_WHITELEVEL, 1, &white_norm);

  // linear DNG: buffer is already at visible dims (post-demosaic);
  // ACTIVEAREA covers the full buffer, no margin to crop
  const uint32_t active_area[4] = {
    0, 0, (uint32_t)height, (uint32_t)width,
  };
  const float default_scale[2] = { 1.0f, 1.0f };
  const float default_crop_origin[2] = { 0.0f, 0.0f };
  const float default_crop_size[2] = { (float)width, (float)height };
  TIFFSetField(tif, TIFFTAG_ACTIVEAREA, active_area);
  TIFFSetField(tif, TIFFTAG_DEFAULTSCALE, default_scale);
  TIFFSetField(tif, TIFFTAG_DEFAULTCROPORIGIN, default_crop_origin);
  TIFFSetField(tif, TIFFTAG_DEFAULTCROPSIZE, default_crop_size);

  // scanline write: float [0, 1] normalized camRGB -> uint16
  //     [0, 65535]. BlackLevel=0 and WhiteLevel=65535 let the
  //     re-importer recover the [0, 1] range via the standard raw
  //     normalization (val - black) / (white - black)
  const float clip_hi = 65535.0f;
  uint16_t *scan = g_malloc((size_t)width * 3 * sizeof(uint16_t));
  int res = 0;
  if(!scan)
  {
    TIFFClose(tif);
    g_unlink(filename);
    return 1;
  }
  for(int y = 0; y < height && res == 0; y++)
  {
    const float *row = rgb + (size_t)y * width * 3;
    for(int x = 0; x < width; x++)
    {
      for(int c = 0; c < 3; c++)
      {
        float adc = row[x * 3 + c] * 65535.0f;
        if(adc < 0.0f) adc = 0.0f;
        if(adc > clip_hi) adc = clip_hi;
        scan[x * 3 + c] = (uint16_t)(adc + 0.5f);
      }
    }
    if(TIFFWriteScanline(tif, scan, y, 0) < 0) res = 1;
  }
  g_free(scan);

  // SubIFD1 = full-res preview when we have both thumb and original
  if(res == 0 && have_thumb)
  {
    if(!TIFFWriteDirectory(tif))
    {
      dt_print(DT_DEBUG_ALWAYS,
               "[imageio_dng] TIFFWriteDirectory failed closing linear SubIFD0");
      res = 1;
    }
    else
    {
      TIFFCreateDirectory(tif);
      _register_extra_dng_fields(tif);
      if(_write_full_preview_subifd(tif, preview) != 0) res = 1;
    }
  }

  TIFFClose(tif);
  g_free(thumb_jpeg);

  if(res == 0 && exif_blob && exif_len > 0)
    dt_exif_write_blob((uint8_t *)exif_blob, (uint32_t)exif_len,
                       filename, 0);

  if(res != 0)
    g_unlink(filename);

  return res;
}

// =============================================================================
// dt_imageio_dng_write_float — hand-rolled byte-level TIFF/DNG writer
//
// Used by HDR merge. Writes a 32-bit float CFA DNG (Bayer or X-Trans).
// The helpers and macros below are private to this writer and are not
// shared with the libtiff-based uint16 writers above
// =============================================================================

// TIFF type codes (libtiff knows these natively, so the uint16 writers
// above don't need them)
#define BYTE 1
#define ASCII 2
#define SHORT 3
#define LONG 4
#define RATIONAL 5
#define SRATIONAL 10

#define HEADBUFFSIZE 1024

static inline void _imageio_dng_write_buf(uint8_t *buf, const uint32_t d, const int val)
{
  if(d + 4 >= HEADBUFFSIZE) return;
  buf[d] = val & 0xff;
  buf[d + 1] = (val >> 8) & 0xff;
  buf[d + 2] = (val >> 16) & 0xff;
  buf[d + 3] = val >> 24;
}

static inline int _imageio_dng_make_tag(
    const uint16_t tag,
    const uint16_t type,
    const uint32_t lng,
    const uint32_t fld,
    uint8_t *buf,
    const uint32_t b,
    uint8_t *cnt)
{
  if(b + 12 < HEADBUFFSIZE)
  {
    _imageio_dng_write_buf(buf, b, (type << 16) | tag);
    _imageio_dng_write_buf(buf, b+4, lng);
    _imageio_dng_write_buf(buf, b+8, fld);
    *cnt = *cnt + 1;
  }
  return b + 12;
}

static inline void _imageio_dng_write_tiff_header(
    FILE *fp,
    uint32_t xs,
    uint32_t ys,
    float Tv,
    float Av,
    float f,
    float iso,
    uint32_t filter,
    const uint8_t xtrans[6][6],
    const float whitelevel,
    const dt_aligned_pixel_t wb_coeffs,
    const float adobe_XYZ_to_CAM[4][3])
{
  const uint32_t channels = 1;
  uint8_t buf[HEADBUFFSIZE];
  uint8_t cnt = 0;

  // this matrix is generic for XYZ->sRGB / D65
  int m[9] = { 3240454, -1537138, -498531, -969266, 1876010, 41556, 55643, -204025, 1057225 };
  int den = 1000000;

  memset(buf, 0, sizeof(buf));
  /* TIFF file header, little-endian */
  buf[0] = 0x49;
  buf[1] = 0x49;
  buf[2] = 0x2a;
  buf[4] = 8;

  // If you want to add other tags written to a dng file include the the ID in the enum to
  // keep track of written tags so we don't a) have leaks or b) overwrite anything in data section
  const int first_tag = __LINE__ + 3;
  enum write_tags
  {
    EXIF_TAG_SUBFILE = 254,           /* New subfile type.  */
    EXIF_TAG_IMGWIDTH = 256,          /* Image width.  */
    EXIF_TAG_IMGLENGTH = 257,         /* Image length.  */
    EXIF_TAG_BPS = 258,               /* Bits per sample: 32-bit float */
    EXIF_TAG_COMPRESS = 259,          /* Compression.  */
    EXIF_TAG_PHOTOMINTREP = 262,      /* Photo interp: CFA  */
    EXIF_TAG_STRIP_OFFSET = 273,      /* Strip offset.  */
    EXIF_TAG_ORIENTATION = 274,       /* Orientation. */
    EXIF_TAG_SAMPLES_PER_PIXEL = 277, /* Samples per pixel.  */
    EXIF_TAG_ROWS_PER_STRIP = 278,    /* Rows per strip.  */
    EXIF_TAG_STRIP_BCOUNT = 279,      /* Strip byte count.  */
    EXIF_TAG_PLANAR_CONFIG = 284,     /* Planar configuration.  */
    EXIF_TAG_SAMPLE_FORMAT = 339,     /* SampleFormat = 3 => ieee floating point */
    EXIF_TAG_REPEAT_PATTERN = 33421,  /* pattern repeat */
    EXIF_TAG_SENS_PATTERN = 33422,    /* sensor pattern */
    EXIF_TAG_VERSION = 50706,         /* DNG Version */
    EXIF_TAG_WHITE_LEVEL = 50717,     /* White level */
    EXIF_TAG_COLOR_MATRIX1 = 50721,   /* ColorMatrix1 (XYZ->native cam) */
    EXIF_TAG_SHOT_NEUTRAL = 50728,    /* AsShotNeutral for rawspeed Dngdecoder camera white balance */
    EXIF_TAG_ILLUMINANT1 = 50778,     /* CalibrationIlluminant1 */
  };
  buf[8] = (uint8_t)(__LINE__ - first_tag - 1); /* number of entries */

  uint32_t b = 10;
  uint32_t data = 10 + buf[8] * 12 + 4; // takes care of the header, entries, and termination

  b = _imageio_dng_make_tag(EXIF_TAG_SUBFILE, LONG, 1, 0, buf, b, &cnt);
  b = _imageio_dng_make_tag(EXIF_TAG_IMGWIDTH, LONG, 1, xs, buf, b, &cnt);
  b = _imageio_dng_make_tag(EXIF_TAG_IMGLENGTH, LONG, 1, ys, buf, b, &cnt);
  b = _imageio_dng_make_tag(EXIF_TAG_BPS, SHORT, 1, 32, buf, b, &cnt);
  b = _imageio_dng_make_tag(EXIF_TAG_COMPRESS, SHORT, 1, 1, buf, b, &cnt);
  b = _imageio_dng_make_tag(EXIF_TAG_PHOTOMINTREP, SHORT, 1, 32803, buf, b, &cnt);
  b = _imageio_dng_make_tag(EXIF_TAG_STRIP_OFFSET, LONG, 1, 0, buf, b, &cnt);
  uint32_t ofst = b - 4; /* remember buffer address for updating strip offset later */
  b = _imageio_dng_make_tag(EXIF_TAG_ORIENTATION, SHORT, 1, 1, buf, b, &cnt);
  b = _imageio_dng_make_tag(EXIF_TAG_SAMPLES_PER_PIXEL, SHORT, 1, channels, buf, b, &cnt);
  b = _imageio_dng_make_tag(EXIF_TAG_ROWS_PER_STRIP, LONG, 1, ys, buf, b, &cnt);
  b = _imageio_dng_make_tag(EXIF_TAG_STRIP_BCOUNT, LONG, 1, (ys * xs * channels*4), buf, b, &cnt);
  b = _imageio_dng_make_tag(EXIF_TAG_PLANAR_CONFIG, SHORT, 1, 1, buf, b, &cnt);
  b = _imageio_dng_make_tag(EXIF_TAG_SAMPLE_FORMAT, SHORT, 1, 3, buf, b, &cnt);

  if(filter == 9u) // xtrans
    b = _imageio_dng_make_tag(EXIF_TAG_REPEAT_PATTERN, SHORT, 2, (6 << 16) | 6, buf, b, &cnt);
  else
    b = _imageio_dng_make_tag(EXIF_TAG_REPEAT_PATTERN, SHORT, 2, (2 << 16) | 2, buf, b, &cnt);

  uint32_t cfapattern = 0;
  switch(filter)
  {
    case 0x94949494:
      cfapattern = (2 << 24) | (1 << 16) | (1 << 8) | 0; // rggb
      break;
    case 0x49494949:
      cfapattern = (1 << 24) | (0 << 16) | (2 << 8) | 1; // gbrg
      break;
    case 0x61616161:
      cfapattern = (1 << 24) | (2 << 16) | (0 << 8) | 1; // grbg
      break;
    default:                                             // case 0x16161616:
      cfapattern = (0 << 24) | (1 << 16) | (1 << 8) | 2; // bggr
      break;
  }

  if(filter == 9u) // xtrans
  {
    b = _imageio_dng_make_tag(EXIF_TAG_SENS_PATTERN, BYTE, 36, data, buf, b, &cnt); /* xtrans PATTERN */
    // apparently this doesn't need byteswap:
    memcpy(buf + data, xtrans, sizeof(uint8_t)*36);
    data += 36;
  }
  else // bayer
    b = _imageio_dng_make_tag(EXIF_TAG_SENS_PATTERN, BYTE, 4, cfapattern, buf, b, &cnt); /* bayer PATTERN */

  b = _imageio_dng_make_tag(EXIF_TAG_VERSION, BYTE, 4, 1 | (4 << 8), buf, b, &cnt);

  // WhiteLevel is straight integer even for float DNGs
  b = _imageio_dng_make_tag(EXIF_TAG_WHITE_LEVEL, LONG, 1, (uint32_t)roundf(whitelevel), buf, b, &cnt);

  // ColorMatrix1 try to get camera matrix else m[k] like before
  if(dt_is_valid_colormatrix(adobe_XYZ_to_CAM[0][0]))
  {
    den = 10000;
    for(int k= 0; k < 3; k++)
      for(int i= 0; i < 3; i++)
        m[k*3+i] = roundf(adobe_XYZ_to_CAM[k][i] * den);
  }
  b = _imageio_dng_make_tag(EXIF_TAG_COLOR_MATRIX1, SRATIONAL, 9, data, buf, b, &cnt); /* ColorMatrix1 (XYZ->native cam) */
  for(int k = 0; k < 9; k++)
  {
    _imageio_dng_write_buf(buf, data + k*8, m[k]);
    _imageio_dng_write_buf(buf, data+4 + k*8, den);
  }
  data += 9 * 8;

  b = _imageio_dng_make_tag(EXIF_TAG_SHOT_NEUTRAL, RATIONAL, 3, data, buf, b, &cnt);
  den = 1000000;
  for(int k = 0; k < 3; k++)
  {
    const float coeff = roundf(((float)den * wb_coeffs[1]) / wb_coeffs[k]);
    _imageio_dng_write_buf(buf, data + k*8, (int)coeff);
    _imageio_dng_write_buf(buf, data+4 + k*8, den);
  }
  data += 3 * 8;

  b = _imageio_dng_make_tag(EXIF_TAG_ILLUMINANT1, SHORT, 1, DT_LS_D65, buf, b, &cnt);

  // We have all tags using data now written so we can finally use strip offset
  _imageio_dng_write_buf(buf, ofst, data);

  /* Termination is implicit: next IFD already 0 when buf initialized */

  if(buf[8] != cnt)
  {
    dt_print(DT_DEBUG_ALWAYS, "[dng_write_header] can't write valid header, unexpected number of entries!");
    return;
  }

  if(data >= HEADBUFFSIZE)
  {
    dt_print(DT_DEBUG_ALWAYS, "[dng_write_header] can't write valid header as it exceeds buffer size!");
    return;
  }

  // exif is written later, by exiv2:
  const int written = fwrite(buf, 1, data, fp);
  if(written != data) dt_print(DT_DEBUG_ALWAYS, "[dng_write_header] failed to write image header!");
}


void dt_imageio_dng_write_float(
    const char *filename, const float *const pixel, const int wd,
    const int ht, void *exif, const int exif_len, const uint32_t filter,
    const uint8_t xtrans[6][6],
    const float whitelevel,
    const dt_aligned_pixel_t wb_coeffs,
    const float adobe_XYZ_to_CAM[4][3])
{
  FILE *f = g_fopen(filename, "wb");
  if(f)
  {
    _imageio_dng_write_tiff_header(f, wd, ht, 1.0f / 100.0f, 1.0f / 4.0f, 50.0f, 100.0f,
                                     filter, xtrans, whitelevel, wb_coeffs, adobe_XYZ_to_CAM);
    const int k = fwrite(pixel, sizeof(float), (size_t)wd * ht, f);
    if(k != wd * ht) dt_print(DT_DEBUG_ALWAYS, "[dng_write] Error writing image data to %s", filename);
    fclose(f);
    if(exif) dt_exif_write_blob(exif, exif_len, filename, 0);
  }
}

#undef BYTE
#undef ASCII
#undef SHORT
#undef LONG
#undef RATIONAL
#undef SRATIONAL
#undef HEADBUFFSIZE

// clang-format off
// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
// vim: shiftwidth=2 expandtab tabstop=2 cindent
// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
// clang-format on
