Paletten-Bitmaps wie GIF oder PNG

C# Quellcode - 11.9 Kb

Worum geht es?

Diese Seite erklärt, wie binäre Daten in einem Paletten-Bild (8 Bits/Pixel) versteckt werden können. Die Methode ist eine ganz andere als die, die vorher für RBG (24 Bits/Pixel) Bitmaps verwendet wurde. Dennoch kann es ganz nützlich sein, den ersten Teil gelesen zu haben, falls Dir die Unterschiede zwischen den beiden Arten von Bitmaps noch nicht bekannt sind.

Erste Gedanken zu Index-Bildern…

Auf den vorhergehenden Seiten haben wir die Bits der geheimen Nachrichten in den unteren Bits der Farbwerte versteckt. In einer indizierten Bitmap stehen die Farbwerte nicht in den Pixeln, sondern in der Palette:

24 Bits pro Pixel, keine Palette


8 Bits pro Pixel, Palette von beliebiger Größe (1 - 256)

Deine erste Idee könnte sein, die Nachricht in der Palette statt in den Pixeln zu verstecken. Aber die Palette enthält 256 oder weniger Farben, sie könnte nur ein paar Bytes verstecken. Dieses Problem führt zur zweiten Idee: Vergrößern wir die Palette doch, kopieren wird die Farbwerte!

Wenn wir die Palette verdoppeln, bekommen wir zwei alternative Indizes für jede Farbe. Hey, das könnte eine bessere Möglichkeit sein! Eine Farbe aus den ersten zwei Zeilen zu referenzieren könnte “Nachrichten-Bit ist 0” heissen, und die gleiche Farbe aus den unteren zwei Zeilen zu referenzieren könnte “Nachrichten-Bit ist 1” bedeuten. Aber wieder gibt es ein Problem: Eine Palette mit doppelten Farben ist Quatsch. Niemand würde jemals so eine Palette verwenden. Eine verdoppelte Palette ist viel zu offensichtlich.

Also können wir die Palette nicht einfach kopieren, aber wir können Farben einfügen, die ein wenig von den vorhandenen Farben abweichen. Angenommen, wir fügen für jede Farbe der Palette zwei ähnliche (aber nicht gleiche) Farbwerte ein:

Jetzt haben wir eine gestreckte Palette, aber das Vorhandensein einer versteckten Nachricht ist genauso offensichtlich, weil nur jede dritte Farbe tatsächlich verwendet wird. Das ist überhaupt kein Problem, wir müssen nur die Pixel ändern, so dass ein paar der Pixel, die eine Original-Farbe referenziert haben, jetzt eine ihrer Kopien referenzieren.

Jetzt können wir etwas in den Pixeln verstecken. Für ein Nachrichten-Bit von “1” lassen wir ein Pixel auf die Original-Farbe verweisen, und für ein Nachrichten-Bit von “0” lassen wir es auf eine der hinzugefügten Farben verweisen. Aber wie sollen wir die versteckte Nachricht wieder auslesen? Wenn wir nur die gestreckte Palette kennen, wissen wir nicht, welche Farben von der Original-Palette kopiert und welche hinzugefügt wurden.

Gibt es eine Möglichkeit, einen Farbwert als “aus der Original-Palette” zu markieren? Tja… da wären die niedrigsten Bits in jeder Farbe. Dieser Name ist nicht ganz richtig. Dank üblicher Bildschirme und schwacher menschlicher Augen verdient das unterste Bits einer Farbkomponente den Namen irrelevantes Bit. Indem wir das erste Bit einer Farbkomponente setzen oder nicht, können wir die Palette strecken und jede Farbe als kopiert oder neu markieren:

Wie man eine Palette un-optimiert

Während wir die neue Palette erstellen, müssen wir die kopierten und eingefügten Farben mitschreiben, weil wir später die Pixel anpassen werden und dafür in der Lage sein müssen, eine Original-Farbe (mit gerader Blaukomponente) für eine “0”, oder eine neue Farbe für eine “1” zu referenzieren. Die Methode StretchPalette verwendet einen Hashtable, um jedem Index der alten Palette die entsprechenden Indizes in der neuen Palette zuzuordnen.

/// <summary>
/// Erstellt eine größere Palette durch Duplizieren
/// und Verändern der Farben einer anderen Palette
/// </summary>
/// <param name="oldPalette">Die Palette, die getreckt werden soll</param>
/// <param name="maxPaletteSize">Anzahl der Farben in der neuen Palette</param>
/// <param name="newPalette">Erhält die neuen Paletten-Einträge</param>
/// <param name="colorIndexToNewIndices">
/// Erhält einen Hashtable mit den Original-Farben als Schlüssel,
/// und den entsprechenden neuen Indizes als Werte
/// </param>
public void StretchPalette(ColorPalette oldPalette, int maxPaletteSize,
  ref ArrayList newPalette, ref Hashtable colorIndexToNewIndices) {

  //collects the new palette entries
  newPalette = new ArrayList(maxPaletteSize);
  //maps each old index to the new indices
  colorIndexToNewIndices = new Hashtable( oldPalette.Entries.Length );

  Random random = new Random();
  byte indexInNewPalette;
  Color color, newColor;
  ColorIndexList colorIndexList;

  //repeat the loop if necessary
  while(newPalette.Count < maxPaletteSize){
          //loop over old palette entries
          for(byte n=0; n<oldPalette.Entries.Length; n++){
            color = oldPalette.Entries[n]; //original color

            if(colorIndexToNewIndices.ContainsKey(n)){
              //this color from the original palette already has
              //one or more copies in the new palette
              colorIndexList = (ColorIndexList)colorIndexToNewIndices[n];
            }else{
              if(color.B%2 > 0){ //make even
                color = Color.FromArgb(color.R, color.G, color.B-1); }

                //add color
                indexInNewPalette = (byte)newPalette.Add(color);
                colorIndexList = new ColorIndexList(random);
                colorIndexList.Add(indexInNewPalette);
                colorIndexToNewIndices.Add(n, colorIndexList);
            }

            if(newPalette.Count < maxPaletteSize){
              //create a non-exact copy of the color
              newColor = GetSimilarColor(random, newPalette, color);

              if(newColor.B%2 == 0){ //make odd
                newColor = Color.FromArgb(
                    newColor.R, newColor.G, newColor.B+1);
              }

              //add the changed color to the new palette
              indexInNewPalette = (byte)newPalette.Add(newColor);
              //add the new index to the list of alternative indices
              colorIndexList.Add(indexInNewPalette);
            }

            //update the Hashtable
            colorIndexToNewIndices[n] = colorIndexList;

            if(newPalette.Count == maxPaletteSize){
              break; //the new palette is full - cancel
            }
    }
  }
}

Falls Du den Code-Ausschnitt gelesen hast (andernfalls kannst du es nachholen, wenn Du den vollständigen Quellcode heruntergeladen hast), hast Du wahrscheinlich die Methode GetSimilarColor bemerkt. Diese Methode gibt eine Variation einer Farbe zurück:

private Color GetSimilarColor(Random random,
        ArrayList excludeColors,
        Color color) {

        Color newColor = color;
        int countLoops = 0, red, green, blue;
        do{
                red = GetSimilarColorComponent(random, newColor.R);
                green = GetSimilarColorComponent(random, newColor.G);
                blue = GetSimilarColorComponent(random, newColor.B);
                newColor = Color.FromArgb(red, green, blue);
                countLoops++;
        //make sure that there are no duplicate colors
        }while(excludeColors.Contains(newColor)&&(countLoops<10));

        return newColor;
}

private byte GetSimilarColorComponent(Random random, byte colorValue){
        if(colorValue < 128){
                colorValue = (byte)(colorValue *
                        (1 + random.Next(1,8)/(float)100) );
        }else{
                colorValue = (byte)(colorValue /
                        (1 + random.Next(1,8)/(float)100) );
        }
        return colorValue;
}

Jetzt haben wir eine neue Palette, und eine Schlüssel/Wert-Tabelle, um alte Indizes ihren neuen Indizes zuzuordnen. Der nächste Schritt ist endlich, die Bits der Nachricht zu verstecken, während das Bild kopiert wird. System.Drawing.Image hat eine Eigenschaft Palette vom Typ ColorPalette. Das ist eine der restriktivsten Klassen, ide ich je gesehen habe. Sie hat zwei Eigenschaften, Flags und Entries - beide sind schreibgeschützt. ColorPalette erlaubt es, die Farben der orhandenen Palette zu ändern, aber wir können keine Farben einfügen. Ich wollte nicht stundenlang nach eine sauberen .NET Lösung suchen, eine neue Bitmap zu schreiben ist einfacher:

/// <summary>
/// Erstellt ein Bild mit gestreckter Palette,
/// konvertiert die Pixel des Original-Bildes für die neue Palette,
/// und versteckt eine Nachricht in den konvertierten Pixeln
/// </summary>
/// <param name="bmp">Original-Bilde</param>
/// <param name="palette">Neue Palette</param>
/// <param name="colorIndexToNewIndices">
/// Hashtable der jedem Index der Original-Palette
/// eine Liste von Indizes in der neuen Palette zuordnet
/// </param>
/// <param name="messageStream">Geheime Nachricht</param>
/// <param name="keyStream">
/// Schlüssel der die Entfernungen zwischen zwei zum
/// Verstecken verwendeten Pixeln angibt
/// </param>
/// <returns>Das neue Bild</returns>
private Bitmap CreateBitmap(
  Bitmap bmp, ArrayList palette,
  Hashtable colorIndexToNewIndices,
  Stream messageStream, Stream keyStream) {

  //lock the original bitmap
  BitmapData bmpData = bmp.LockBits(
    new Rectangle(0,0,bmp.Width, bmp.Height),
    ImageLockMode.ReadWrite,
    PixelFormat.Format8bppIndexed);

  //size of the image data in bytes
  int imageSize = (bmpData.Height * bmpData.Stride)+(palette.Count * 4);

  //copy all pixels
  byte[] pixels = new byte[imageSize];
  Marshal.Copy(bmpData.Scan0, pixels, 0, (bmpData.Height*bmpData.Stride));

  int messageByte=0, messageBitIndex=7;
  bool messageBit;
  ColorIndexList newColorIndices;
  Random random = new Random();

  //index of the next pixel that's going to hide one bit
  int nextUseablePixelIndex = GetKey(keyStream);

  //loop over the pixels
  for(int pixelIndex=0; pixelIndex<pixels.Length; pixelIndex++){

    //get the list of new color indices for the current pixel
    newColorIndices=(ColorIndexList)colorIndexToNewIndices[pixels[pixelIndex]];

    if((pixelIndex < nextUseablePixelIndex) || messageByte < 0){
    //message complete or this pixel has to be skipped - use a random color
      pixels[pixelIndex] = newColorIndices.GetIndex();
    }else{
      //message not complete yet

      if(messageBitIndex == 7){
        //one byte has been hidden - proceed to the next one
        messageBitIndex = 0;
        messageByte = messageStream.ReadByte();
      }else{
        messageBitIndex++; //next bit
      }

      //get a bit out of the current byte
      messageBit = (messageByte & (1 << messageBitIndex)) > 0;
      //get the index of a similar color in the new palette
      pixels[pixelIndex] = newColorIndices.GetIndex(messageBit);
      nextUseablePixelIndex += GetKey(keyStream);
    }
  }

  //Jetzt haben wir die Palette und die neuen Pixel.
  //Genug Informationen um die Bitmap zu schreiben !
</b>
  BinaryWriter bw = new BinaryWriter( new MemoryStream() );

  //write bitmap file header
  //...
  //...

  //write bitmap info header
  //...
  //...

  //write palette
  foreach(Color color in palette){
    bw.Write((UInt32)color.ToArgb());
  }

  //write pixels
  bw.Write(pixels);

  bmp.UnlockBits(bmpData);

  Bitmap newImage = (Bitmap)Image.FromStream(bw.BaseStream);
  newImage.RotateFlip(RotateFlipType.RotateNoneFlipY);

  bw.Close();
  return newImage;
}

Eine versteckte Nachricht auslesen

Eine versteckte Nachricht auszulesen ist viel einfacher, als sie zu verstecken. Es gibt nur eine Palette, und wir müssen uns nicht mit alten und neuen Indizes herumschlagen. Wir verwenden einfach den Verteilungsschlüssel um ein Träger-Pixel zu finden, prüfen die referenzierte Farbe nach gerader oder ungerade Blaukomponente, speichern das gefundene Bit (welches color.B % 2 > 0 ist) und machen mit dem nächsten Pixel weiter, bis dsie Nachricht vollständig ist:

public void Extract(Stream messageStream, Stream keyStream){
  //load the carrier image
  Bitmap bmp = new Bitmap(sourceFileName);
  BitmapData bmpData = bmp.LockBits(
    new Rectangle(0,0,bmp.Width, bmp.Height),
    ImageLockMode.ReadWrite,
    PixelFormat.Format8bppIndexed);

  //copy all pixels
  byte[] pixels = new byte[bmpData.Stride*bmpData.Height];
  Marshal.Copy(bmpData.Scan0, pixels, 0, pixels.Length);

  Color[] palette = bmp.Palette.Entries;
  byte messageByte=0, messageBitIndex=0, pixel=0;
  int messageLength=0, pixelIndex=0;

  //read pixels until the message is complete
  while((messageLength==0) || (messageStream.Length < messageLength)){
    //locate the next pixel that carries a hidden bit
    pixelIndex += GetKey(keyStream);
    pixel = pixels[pixelIndex];

    if( (palette[pixel].B % 2) == 1 ){
      //odd blue-component: message-bit was "1"
      messageByte += (byte)(1 << messageBitIndex);
    } //else: messageBit was "0", nothing to do

    if(messageBitIndex == 7){ //a byte is complete
      //save and reset messageByte, reset messageBitIndex
      messageStream.WriteByte(messageByte);
      messageBitIndex = 0;
      messageByte = 0;

      if((messageLength == 0)&&(messageStream.Length==4)){
        //message's length has been read
        messageStream.Seek(0, SeekOrigin.Begin);
        messageLength = new BinaryReader(messageStream).ReadInt32();
        messageStream.SetLength(0);
      }
    }else{
      messageBitIndex++; //next bit
    }
  }

  //release the carrier bitmap
  bmp.UnlockBits(bmpData);
  bmp.Dispose();
}

Beispiel

Die Bilder dürfen nur 128 oder weniger Farben enthalten, sonst könnten wir nicht für jede Farbe einen alternativen Paletteneintrag hinzufügen. Für eine ernsthafte Einschränkung halte ich das jedoch nicht, denn die meisten indizierten GIF oder PNG Bilder haben weniger Farben. Wenn alle 256 Farben für ein Bild gebraucht werden, ist ein Format mit 24 Bits/Pixel besser geeignet. Aber jetzt schauen wir endlich zu, wie sich die Palette verändert…

Das ist Sternchen Kanari, unser Modell:

Hier sind das gleiche Foto als indiziertes PNG mit 64 Farben und seine Palette:

Starte die Demo-Anwendung, wähle das Bild als Träger und irgendeine andere Datei als Schlüssel…

…und klick Hide. Das generierte Bild enthält eine Palette mit 192 Farben, und sieht nicht viel anders aus, obwohl alle Farben verwendet werden und keine sich wiederholt.

Das generierte Bild das Du hier siehst enthält eine versteckte Nachricht von 42 Wörtern, die mit der Demo-Anwendung ausgelesen werden kann. Der Schlüssel zur Nachricht steckt irgendwo im fünften Bild dieser Seite. Du brauchst einen Hexadezimal-Editor, um seine 11 Bytes in eine Schlüssel-Datei zu tippen…

Follow me on Mastodon