content: /*
   *  Licensed to the Apache Software Foundation (ASF) under one
   *  or more contributor license agreements.  See the NOTICE file
   *  distributed with this work for additional information
   *  regarding copyright ownership.  The ASF licenses this file
   *  to you under the Apache License, Version 2.0 (the
   *  "License"); you may not use this file except in compliance
   *  with the License.  You may obtain a copy of the License at
   *
   *        http://www.apache.org/licenses/LICENSE-2.0
   *
   *  Unless required by applicable law or agreed to in writing,
   *  software distributed under the License is distributed on an
   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   *  KIND, either express or implied.  See the License for the
   *  specific language governing permissions and limitations
   *  under the License.
   */
  package org.apache.causeway.commons.internal.image;
  
  import java.awt.Graphics2D;
  import java.awt.Image;
  import java.awt.image.BufferedImage;
  import java.awt.image.DataBufferInt;
  import java.awt.image.DirectColorModel;
  import java.awt.image.Raster;
  import java.awt.image.SinglePixelPackedSampleModel;
  import java.awt.image.WritableRaster;
  import java.io.ByteArrayInputStream;
  import java.io.ByteArrayOutputStream;
  import java.nio.charset.StandardCharsets;
  
  import javax.imageio.ImageIO;
  
  import org.jspecify.annotations.Nullable;
  
  import org.apache.causeway.commons.internal.base._Bytes;
  import org.apache.causeway.commons.internal.base._NullSafe;
  import org.apache.causeway.commons.internal.primitives._Ints;
  
  import org.jspecify.annotations.NonNull;
  import lombok.SneakyThrows;
  
  public class _Images {
  
      // -- BYTES
  
      @SneakyThrows
      public static byte[] toBytes(final @NonNull BufferedImage image){
          try(var bos = new ByteArrayOutputStream(8 * 1024)) {
              ImageIO.write(image, "png", bos); // png is lossless
              return bos.toByteArray();
          }
      }
  
      @SneakyThrows
      public static BufferedImage fromBytes(final @NonNull byte[] imageData){
          try(var bis = new ByteArrayInputStream(imageData)){
              return ImageIO.read(bis);
          }
      }
  
      // -- BASE64
  
      @SneakyThrows
      public static String toBase64(final @NonNull BufferedImage image){
          return new String(_Bytes.asUrlBase64.apply(toBytes(image)),
                  StandardCharsets.UTF_8);
      }
  
      @SneakyThrows
      public static BufferedImage fromBase64(final @NonNull String base64ImageData){
          var imageData = _Bytes.ofUrlBase64.apply(base64ImageData.getBytes(StandardCharsets.UTF_8));
          return fromBytes(imageData);
      }
  
      // -- PIXELS
  
      /**
       * @return 2 dim. array of pixels defining this image,
       *      where each pixel is a 32 bit ARGB color value,
       *      with {@code A} the alpha value as highest significant 8 bits
       *      followed by {@code R} the red value and so on
       */
      @Nullable
      public static int[][] toPixels(final @Nullable BufferedImage image){
  
          if(image==null) {
              return null;
          }
  
          final int width = image.getWidth();
          final int height = image.getHeight();
          var pixels = new int[height][width];
          for(int lineIndex=0; lineIndex<height; ++lineIndex) {
              image.getRGB(0, lineIndex, width, 1, pixels[lineIndex], 0, width);
  
              // debug
              // System.err.println(
              //           _Ints.rowForm(pixels[lineIndex],10, Integer::toHexString));
  
          }
          return pixels;
      }
  
      /**
       * @param pixels - 2 dim. array of pixels defining this image,
       *      where each pixel is a 32 bit ARGB color value,
       *      with {@code A} the alpha value as highest significant 8 bits
       *      followed by {@code R} the red value and so on
       */
      @Nullable
      public static BufferedImage fromPixels(final @Nullable int[][] pixels){
  
          /*sonar-ignore-on*/
          final int height = _NullSafe.size(pixels);
          final int width = height>0
                  ? _NullSafe.size(pixels[0])
                  : 0;
          /*sonar-ignore-off*/
  
          final int pixelCount = width * height;
  
          if(pixelCount>0) {
              // internally clones pixel data
              var raster = createRasterARGB8888(pixels);
              return createImageARGB8888(raster);
          } else {
              return null;
          }
      }
  
      // -- RASTER UTILS (LOW LEVEL)
  
      private static WritableRaster createRasterARGB8888(final int[][] pixels){
          final int height = pixels.length;
          final int width = pixels[0].length;
          return createRasterARGB8888(width, height, _Ints.flatten(pixels));
      }
  
      private static WritableRaster createRasterARGB8888(final int width, final int height, final int[] dataArray){
          var dataBuffer = new DataBufferInt(dataArray, width * height);
          var sampleModel = new SinglePixelPackedSampleModel(
                  dataBuffer.getDataType(), width, height, BitMask8888);
          return Raster.createWritableRaster(sampleModel, dataBuffer, null);
      }
  
      private static BufferedImage createImageARGB8888(final WritableRaster raster){
          var directColorModel​ =
                  new DirectColorModel(32, BitMask8888[0], BitMask8888[1], BitMask8888[2], BitMask8888[3]);
          return new BufferedImage(directColorModel​, raster, false, null);
      }
  
      private final static int[] BitMask8888 = {
              0xff<<16,
              0xff<<8,
              0xff,
              0xff<<24,
      };
  
      public static int compare(final BufferedImage a, final BufferedImage b) {
          int c = Integer.compare(a.getWidth(), b.getWidth());
          if(c!=0) {
              return c;
          }
          c = Integer.compare(a.getHeight(), b.getHeight());
          if(c!=0) {
              return c;
          }
          for (int x = 0; x < a.getWidth(); x++) {
              for (int y = 0; y < a.getHeight(); y++) {
                  c = Integer.compare(a.getRGB(x, y), b.getRGB(x, y));
                  if(c!=0) {
                      return c;
                  }
              }
          }
          return 0;
      }
  
      // -- RESIZE
      
      public static BufferedImage resizeToMaxHeight(BufferedImage originalImage, int maxHeight) {
          if(originalImage.getHeight()<=maxHeight) return originalImage; 
          
          int targetHeight = maxHeight;
          int targetWidth = (int)((long)(originalImage.getWidth() * targetHeight) / originalImage.getHeight());
  
          // Scale the image
          var scaledImage = originalImage.getScaledInstance(targetWidth, targetHeight, Image.SCALE_SMOOTH);
          var virtualImage = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_ARGB);
          
          return draw(scaledImage, virtualImage);
      }
      
      // -- HELPER
      
      /**
       * Draws given image onto given bufferedImage and returns the latter. 
       */
      private static BufferedImage draw(Image image, BufferedImage bufferedImage) {
          Graphics2D g2d = bufferedImage.createGraphics();
          g2d.drawImage(image, 0, 0, null);
          g2d.dispose();
          return bufferedImage;
      }
  
  }
  