void PaletteEditor::stopsChangedAction()
{
	static const int GradientBufferLastIdx = GradientBufferSize - 1;
	static const qreal dx  = 1.0 / GradientBufferSize;
	QSize s( m_gradientLabel->maximumSize() );
	QRect r( QPoint(0, 0), QSize(s.width(), (s.height() / 2.0 ) ) );
	QImage palette_image(s, QImage::Format_RGB32);
	QPainter painter(&palette_image);
	GradientStops stops(m_gradientStops->getStops());
	qStableSort(stops.begin(), stops.end(), GradientStop::lessThanGradientStopComparison);

	// now apply the ends and update the palette
	GradientStops ends( m_gradientEnds->getStops() );
	QGradient::Spread spread((QGradient::Spread)m_gradientSpreadGroup->checkedId());

	GradientStop n_stop(stops.first());
	QRgb ccolor = n_stop.second.rgba();
	for (int n = 0, fpos = n_stop.first * GradientBufferSize  ; n < fpos ; n++)
		m_gradient[qMin(n, GradientBufferLastIdx)] = ccolor;

	int last_stop_idx = stops.size() - 1;
	for (int begin_idx = 0; begin_idx < last_stop_idx ; begin_idx++)
	{
		GradientStop a = stops.at(begin_idx);
		GradientStop b = stops.at(begin_idx + 1);
		QColor ac = a.second;
		QColor bc = b.second;
		qreal d = ( b.first - a.first );
		qreal rdx, gdx, bdx, adx;
		if (b.colorspace == 0)
		{
			rdx = ( (bc.red()   - ac.red() )   / d ) * dx;
			gdx = ( (bc.green() - ac.green() ) / d ) * dx;
			bdx = ( (bc.blue()  - ac.blue() )  / d ) * dx;
			adx = ( (bc.alpha() - ac.alpha() ) / d ) * dx;
		}
		else
		{
			rdx = ( (bc.hue()        - ac.hue() )        / d ) * dx;
			gdx = ( (bc.saturation() - ac.saturation() ) / d ) * dx;
			bdx = ( (bc.value()      - ac.value() )      / d ) * dx;
			adx = ( (bc.alpha()      - ac.alpha() )      / d ) * dx;

			if (b.colorspace == 1)
			{
				if (rdx == 0.0)
					rdx = 180.0 / d * dx;
				else if (rdx < 0)
					rdx *= -1;
			}
			else
			{
				if (rdx == 0.0)
					rdx = -180.0 / d * dx;
				else if (rdx > 0)
					rdx *= -1;
			}
		}
		int n  = a.first * GradientBufferSize ;
		int nb = (int)(b.first * GradientBufferSize );
		for (int i = 0 ; n < nb ; i++, n++)
		{
			if (b.colorspace == 0)
			{
				m_gradient[n] = qRgba(
						qBound(0, (int)( ac.red()   + rdx * i + 0.5 ), 255),
						qBound(0, (int)( ac.green() + gdx * i + 0.5 ), 255),
						qBound(0, (int)( ac.blue()  + bdx * i + 0.5 ), 255),
						qBound(0, (int)( ac.alpha() + adx * i + 0.5 ), 255));
			}
			else
			{
				int h = (int)( ac.hue() + rdx * i + 0.5 );
				if (h < 0)
					h += 360;
				m_gradient[n] = QColor::fromHsv(h % 360,
						qBound(0, (int)( ac.saturation() + gdx * i + 0.5 ), 255),
						qBound(0, (int)( ac.value()  + bdx * i + 0.5 ), 255),
						qBound(0, (int)( ac.alpha() + adx * i + 0.5 ), 255)).rgba();
			}
		}
	}

	n_stop = stops.last();
	ccolor = n_stop.second.rgba();
	for (int n = n_stop.first * GradientBufferSize ; n < GradientBufferSize ; n++)
		m_gradient[n] = ccolor;

	qreal start(ends.at(0).first);
	qreal end(ends.at(1).first);
	int begin_idx = start * 256 ;
	int end_idx   = end   * 256 ;
	int ibuf_size = end_idx - begin_idx;
	flam3_palette_entry* ibuf = new flam3_palette_entry[ibuf_size]();

	// a very acute filter
	qreal c2 = 0.01;
	qreal c3 = 1.0;
	qreal c4 = 0.01;
	qreal norm = c2 + c3 + c4;
	qreal k = 0.0;
	qreal skip( (GradientBufferSize / 256.0) / qMax(qreal(1.0/GradientBufferSize), end - start) );
	for (int n = 0 ; n < ibuf_size ; n++, k += skip)
	{
		int j = k;
		QRgb a2( m_gradient[qBound(0, j + 0, GradientBufferLastIdx)] );
		QRgb a3( m_gradient[qMin(j + 1, GradientBufferLastIdx)] );
		QRgb a4( m_gradient[qMin(j + 2, GradientBufferLastIdx)] );

		ibuf[n].color[0] = (qRed(a2)*c2   + qRed(a3)*c3   + qRed(a4)*c4 )   / (norm * 255.);
		ibuf[n].color[1] = (qGreen(a2)*c2 + qGreen(a3)*c3 + qGreen(a4)*c4 ) / (norm * 255.);
		ibuf[n].color[2] = (qBlue(a2)*c2  + qBlue(a3)*c3  + qBlue(a4)*c4 )  / (norm * 255.);
		ibuf[n].color[3] = (qAlpha(a2)*c2 + qAlpha(a3)*c3 + qAlpha(a4)*c4 ) / (norm * 255.);
	}

	// update the gradient editor label
	painter.fillRect(QRect(QPoint(0,0), s), checkers);
	if (ibuf_size == 256)
	{
		for (int n = 0, h = s.height() ; n < 256 ; n++)
		{
			painter.setPen(QColor::fromRgbF(ibuf[n].color[0], ibuf[n].color[1], ibuf[n].color[2], ibuf[n].color[3]));
			painter.drawLine(n, 0, n, h);
		}
	}
	else
	{
		for (int n = 0, h = s.height(), j = 0 ; n < 256 ; n++, j += 4)
		{
			QRgb a2( m_gradient[qBound(0, j + 0, GradientBufferLastIdx)] );
			QRgb a3( m_gradient[qMin(j + 1, GradientBufferLastIdx)] );
			QRgb a4( m_gradient[qMin(j + 2, GradientBufferLastIdx)] );
			QRgb r((qRed(a2)*c2   + qRed(a3)*c3   + qRed(a4)*c4 )   / norm );
			QRgb g((qGreen(a2)*c2 + qGreen(a3)*c3 + qGreen(a4)*c4 ) / norm );
			QRgb b((qBlue(a2)*c2  + qBlue(a3)*c3  + qBlue(a4)*c4 )  / norm );
			QRgb a((qAlpha(a2)*c2 + qAlpha(a3)*c3 + qAlpha(a4)*c4 ) / norm );
			QColor c(r, g, b, a);
			painter.setPen(c);
			painter.drawLine(n, 0, n, h);
		}
	}
	m_gradientLabel->setPixmap(QPixmap::fromImage(palette_image));

	// Rescale the gradient colors into the palette with a simple filter
	if (spread == QGradient::PadSpread)
	{
		QRgb fc(m_gradient[0]);
		flam3_palette_entry e = { 0., { qRed(fc)/255., qGreen(fc)/255., qBlue(fc)/255., qAlpha(fc)/255. }};
		for (int n = 0 ; n < begin_idx ; n++)
			p[n] = e;

		for (int n = begin_idx, j = 0 ; n < end_idx ; n++, j++)
			p[n] = ibuf[j];

		fc = m_gradient[GradientBufferLastIdx];
		e = (flam3_palette_entry){ 1., { qRed(fc)/255., qGreen(fc)/ 255., qBlue(fc)/255., qAlpha(fc)/255. }};
		for (int n = end_idx ; n < 256 ; n++)
			p[n] = e;
	}
	else if (spread == QGradient::RepeatSpread)
	{
		for (int n = begin_idx, j = 0 ; n < 256 ; n++, j++)
			p[n] = ibuf[j % ibuf_size];
		for (int n = begin_idx - 1, j = ibuf_size * 4096 - 1 ; n >= 0 ; n--, j--)
			p[n] = ibuf[j % ibuf_size];
	}
	else if (spread == QGradient::ReflectSpread)
	{
		for (int n = begin_idx, j = 0, h = 4096*ibuf_size -1 ; n < begin_idx + ibuf_size ; n++, j++, h--)
		{
			for (int k = n, q = n + ibuf_size ; k < 256 ; k += 2*ibuf_size, q += 2*ibuf_size )
			{
				p[k] = ibuf[j % ibuf_size];
				if (q < 256)
					p[q] = ibuf[h % ibuf_size];
			}
		}
		for (int n = begin_idx - 1, j = ibuf_size * 4096 - 1, h = 0 ; n >= begin_idx - ibuf_size ; n--, j--, h++)
		{
			for (int k = n, q = n - ibuf_size ; k >= 0 ; k -= 2*ibuf_size, q -= 2*ibuf_size )
			{
				p[k] = ibuf[h % ibuf_size];
				if (q >= 0)
					p[q] = ibuf[j % ibuf_size];
			}
		}
	}
	delete[] ibuf;

	setPaletteView();
	emit paletteChanged();
}
int sfc_palette(int argc, char* argv[]) {
  SfcPalette::Settings settings = {};
  bool verbose = false;
  bool col0_forced = false;
  rgba_t col0 = 0;

  try {
    bool help = false;
    std::string mode_str;
    bool dummy = false;

    Options options;
    options.IndentDescription = sfc::Constants::options_indent;
    options.Header = "Usage: superfamiconv palette [<options>]\n";

    // clang-format off
    options.Add(settings.in_image,           'i', "in-image",       "Input: image");
    options.Add(settings.out_data,           'd', "out-data",       "Output: native data");
    options.Add(settings.out_act,            'a', "out-act",        "Output: photoshop palette");
    options.Add(settings.out_json,           'j', "out-json",       "Output: json");
    options.Add(settings.out_image,          'o', "out-image",      "Output: image");

    options.Add(mode_str,                    'M', "mode",           "Mode",                             std::string("snes"), "Settings");
    options.Add(settings.palettes,           'P', "palettes",       "Number of subpalettes",            unsigned(8),         "Settings");
    options.Add(settings.colors,             'C', "colors",         "Colors per subpalette",            unsigned(16),        "Settings");
    options.Add(settings.tile_w,             'W', "tile-width",     "Tile width",                       unsigned(8),         "Settings");
    options.Add(settings.tile_h,             'H', "tile-height",    "Tile height",                      unsigned(8),         "Settings");
    options.AddSwitch(settings.no_remap,     'R', "no-remap",       "Don't remap colors",               false,               "Settings");
    options.Add(settings.color_zero,         '0', "color-zero",     "Set color #0",                     std::string(),       "Settings");

    options.AddSwitch(verbose,               'v', "verbose",        "Verbose logging", false, "_");
    options.AddSwitch(help,                  'h', "help",           "Show this help",  false, "_");
    options.AddSwitch(dummy,                 '9', std::string(),    std::string(),     false);
    // clang-format on

    if (!options.Parse(argc, argv)) return 1;

    if (argc <= 2 || help) {
      fmt::print(options.Usage());
      return 0;
    }

    settings.mode = sfc::mode(mode_str);

    // Mode-specific defaults
    if (!options.WasSet("palettes")) settings.palettes = sfc::default_palette_count_for_mode(settings.mode);
    if (!options.WasSet("colors")) settings.colors = sfc::palette_size_at_bpp(sfc::default_bpp_for_mode(settings.mode));

    if (!settings.color_zero.empty()) {
      col0 = sfc::from_hexstring(settings.color_zero);
      col0_forced = true;
    }

  } catch (const std::exception& e) {
    fmt::print(stderr, "Error: {}\n", e.what());
    return 1;
  }

  try {
    if (settings.in_image.empty()) throw std::runtime_error("Input image required");

    sfc::Image image(settings.in_image);
    if (verbose) fmt::print("Loaded image from \"{}\" ({})\n", settings.in_image, image.description());

    sfc::Palette palette;

    if (settings.no_remap) {
      if (image.palette_size() == 0) throw std::runtime_error("no-remap requires indexed color image");
      if (verbose) fmt::print("Mapping palette straight from indexed color image\n");

      palette = sfc::Palette(settings.mode, 1, (unsigned)image.palette_size());
      palette.add_colors(image.palette());

    } else {
      if (verbose) fmt::print("Mapping optimized palette ({}x{} entries)\n", settings.palettes, settings.colors);

      palette = sfc::Palette(settings.mode, settings.palettes, settings.colors);

      col0 = col0_forced ? col0 : image.crop(0, 0, 1, 1).rgba_data()[0];

      if (col0_forced || sfc::col0_is_shared_for_mode(settings.mode)) {
        if (verbose) fmt::print("Setting color zero to {}\n", sfc::to_hexstring(col0, true, true));
        palette.set_col0(col0);
      }

      palette.add_images(image.crops(settings.tile_w, settings.tile_h));
    }

    if (verbose) fmt::print("Created palette with {}\n", palette.description());

    if (!settings.no_remap) {
      palette.sort();
    }

    if (!settings.out_data.empty()) {
      palette.save(settings.out_data);
      if (verbose) fmt::print("Saved native palette data to \"{}\"\n", settings.out_data);
    }

    if (!settings.out_act.empty()) {
      palette.save_act(settings.out_act);
      if (verbose) fmt::print("Saved photoshop palette to \"{}\"\n", settings.out_act);
    }

    if (!settings.out_image.empty()) {
      sfc::Image palette_image(palette);
      palette_image.save(settings.out_image);
      if (verbose) fmt::print("Saved palette image to \"{}\"\n", settings.out_image);
    }

    if (!settings.out_json.empty()) {
      sfc::write_file(settings.out_json, palette.to_json());
      if (verbose) fmt::print("Saved json data to \"{}\"\n", settings.out_json);
    }

  } catch (const std::exception& e) {
    fmt::print(stderr, "Error: {}\n", e.what());
    return 1;
  }

  return 0;
}