ubuntuusers.de

Am Sonntag, 24.11, wird gegen 16 Uhr eine Inyoka-Version ausgerollt. Das Portal kann für ein paar Minuten nicht erreichbar sein.

Farbiger Screendump von ttyX ?

Status: Gelöst | Ubuntu-Version: Ubuntu
Antworten |

Pumbaa80 Team-Icon

Avatar von Pumbaa80

Anmeldungsdatum:
5. März 2007

Beiträge: 2130

Wohnort: Residenz des Rechts

Ich hätte gern einen (ANSI-)Screendump der Textkonsole ttyX mit Escape-Sequenzen.
D.h. wenn ich beispielsweise in die Datei screendump speichere und dann cat screendump eingebe, soll der Text genau so ausgegeben werden, wie er vorher zu sehen war.

Folgendes ist mir bekannt:

  • Mit fbgrab kann man den Inhalt der ttyX als Bild speichern, das will ich aber nicht.

  • Mit sudo cp /dev/vcsX ~/screendump kann man schwarz-weiße Screendumps erstellen.

  • Mit sudo cp /dev/vcsaX ~/screendump kann man Screendumps erstellen, die zusätzlich Farbinformationen enthalten.

Letzteres ist schon fast das, was ich suche. Leider sind die Farbattribute nicht als Escape-Sequenzen gespeichert, sondern als Binärdaten zwischen den einzelnen Zeichen.
Außerdem fehlen Zeilenumbrüche.
Die ersten vier Zeichen von /dev/vcsaX enthalten Informationen über die Breite und Höhe der Konsole sowie die Cursorposition, dann folgen abwechselnd jeweils ein Zeichen und der zugehörige Farbwert.

Ich suche also ein Programm, das die ersten vier Bytes (eigentlich genügt das erste) aus der Datei screendump ausliest, die Zeilenumbrüche setzt und schließlich die Farbcodes in Escape-Sequenzen umwandelt.
So was ähnliches wie vcsadump, nur halt wie gesagt keine HTML-Ausgabe, sondern ANSI.

Gibt es so ein Programm schon? Wenn nein: Lässt sich sowas mit einem ein Skript (sed, fold,...) bewerkstelligen? (Und wenn ja: kann mir dabei bitte jemand helfen? 😉)
Alternativ hatte ich mir überlegt, den Quellcode von vcsadump entsprechend zu modifizieren, aber leider sind meine C-Kenntnisse unter aller Sau...

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4638

Wohnort: Berlin

Kleines Python-Programm:

#!/usr/bin/python 
# -*- coding: utf-8 -*- 
import struct
from itertools import imap, islice

# 
# Mapping from VGA text attribute colors to ANSI colors.
# 
VGA_TO_ANSI = (0, 4, 2, 6, 1, 5, 3, 7)


def character2ansi_sequence((character, attribute)):
    """Converts the (character, attribute) tuple into a string with an
    ANSI sequence for the `attribute` followed by the `character`.
    
    `attribute` is a one byte string with a VGA text attribute.
    """
    result = list()
    # 
    # Extract foreground and background color from attribute.
    # 
    value = ord(attribute)
    foreground = value & 7
    background = (value >> 4) & 7
    # 
    # Check for the "bold" bit.
    # 
    if not value & 16:
        result.append(1)
    else:
        result.append(0)
    # 
    # Add the colors.
    # 
    result.append(VGA_TO_ANSI[foreground] + 30)
    result.append(VGA_TO_ANSI[background] + 40)
    return '\033[%sm%s' % (';'.join(map(str, result)), character)


def iter_lines(fileobj):
    """Iterates over the lines of the screendump file.
    
    Returns an iterator over a list of (character, attribute) tuples per
    line of the screendump.  Both elements of the tuples are one byte strings.
    """
    line_length = struct.unpack('xB2x', fileobj.read(4))[0]
    line_size = line_length * 2
    for line in iter(lambda: fileobj.read(line_size), ''):
        yield zip(islice(line, 0, None, 2), islice(line, 1, None, 2))


def main():
    """Test code."""
    dump_file = open('test.dat', 'rb')
    for line in iter_lines(dump_file):
        print ''.join(imap(character2ansi_sequence, line))
    dump_file.close()


if __name__ == '__main__':
    main()

Könnte man natürlich etwas intelligenter gestalten ─ so wird für jedes Zeichen eine ANSI-Escapesequenz erzeugt. Das ist ja nur wirklich nötig wenn sich die Werte ändern. Und man könnte die Leerzeichen an den Zeilenenden entfernen, wenn die Hintergrundfarbe der des Terminals entspricht.

Ausserdem müsste man mit dem höchstwertigen Bit noch etwas anstellen, sollte es gesetzt sein. Ich glaube das war für blinkenden Text!?

user_unknown

Avatar von user_unknown

Anmeldungsdatum:
10. August 2005

Beiträge: 17576

Wohnort: Berlin

Ich hatte vor einiger Zeit mal einen Prototypen, um Console-Sessions aufzuzeichnen und abzuspielen.
Wie's so kommt in Java.
Die Zeilenumbrüche werden nicht gesetzt (es sind ja auch nicht immer welche).
Aber die Farbwiederholung ist implement:

Auch die Farbumsetzung VGA_TO_ANSI habe ich nicht - vielleicht sieht es deshalb so komisch aus:)

import java.util.*;
import java.text.*;
import java.io.*;

/**
        AnsiPlayback

        @author Stefan Wagner
        @date Do 14 Dez 01:08:47 CET 2006

*/
public class AnsiPlayback
{
        public AnsiPlayback (BufferedReader br) throws IOException
        {
                char [] cbuf = new char [4004];
                br.read (cbuf, 0, 4004);
                byte farbe = -1;
                byte oldfarbe = -1;
                byte fg = -1;
                byte bg = -1;


                StringBuffer sb = new StringBuffer (2000);

                for (int i = 4; i < 4004; i+=2)
                {
                        farbe = (byte) cbuf[i+1]; // z.B. 70
                        char zeichen = cbuf[i];
                        // don't distinguish fg/bg-changes:
                        if (farbe != oldfarbe)
                        {
                                fg = (byte) (farbe / 16);
                                bg = (byte) (farbe % 8);
                                // System.out.println (farbe + " = " + fg + " " + bg);

                                sb.append (ansifarbe (fg, bg));
                                oldfarbe = farbe;
                        }
                        sb.append (zeichen);
                }
                System.out.print (sb);
        }

        public static String ansifarbe (int fg, int bg)
        {
                return "\u001b[4"+ fg + ";3" + bg + "m";
        }

        public static void main (String args[]) throws FileNotFoundException, IOException
        {
                BufferedReader br = null;
                if (args.length == 1)
                {
                        br = new BufferedReader (new FileReader (args[0]));
                }
                else br =  new BufferedReader (new InputStreamReader (System.in));
                new AnsiPlayback (br);
        }

        public static void usage ()
        {
                System.out.println ("Usage:\tjava AnsiPlayback param");
        }
}

Pumbaa80 Team-Icon

(Themenstarter)
Avatar von Pumbaa80

Anmeldungsdatum:
5. März 2007

Beiträge: 2130

Wohnort: Residenz des Rechts

Marc 'BlackJack' Rintsch hat geschrieben:

Kleines Python-Programm:

Seeeeehr schön! Vielen Dank! 😀 Das gilt natürlich auch für user unknown , aber ein Python-Skript ist mir im Augenblick lieber als Java.

Marc 'BlackJack' Rintsch hat geschrieben:

Könnte man natürlich etwas intelligenter gestalten ─ so wird für jedes Zeichen eine ANSI-Escapesequenz erzeugt. Das ist ja nur wirklich nötig wenn sich die Werte ändern.

Erledigt.

Marc 'BlackJack' Rintsch hat geschrieben:

Und man könnte die Leerzeichen an den Zeilenenden entfernen, wenn die Hintergrundfarbe der des Terminals entspricht.

Hm, nein, das lieber nicht.

Marc 'BlackJack' Rintsch hat geschrieben:

Ausserdem müsste man mit dem höchstwertigen Bit noch etwas anstellen, sollte es gesetzt sein. Ich glaube das war für blinkenden Text!?

Das hängt wohl vom System ab. Eine exakte "Rückübersetzung" ist nicht möglich, da für die 3 ANSI-Attribute "fett", "blinken" und "unterstreichen" nur 2 Bits zur Verfügung stehen.

Bei mir gilt in der ttyX:
ANSI "fett" → helle Schrift (Bit "8" gesetzt)
ANSI "blinken" → heller Hintergrund (Bit "128" gesetzt)
ANSI "unterstrichen" → weißer Text + Fettschrift invertiert (Bits "1","2","4" gesetzt; Bit "8" invertiert)

Wie man sieht, lässt sich das ANSI-Attribut "unterstrichen" in meinem Fall nicht wiederherstellen. Aber das ist nicht weiter schlimm, da mich die Bildschirmausgabe und nicht die Eingangsparameter interessieren.

Also habe ich mir erlaubt, das Skript wie folgt anzupassen:

#!/usr/bin/python
# -*- coding: utf-8 -*-
import struct
from itertools import imap, islice

#
# Mapping from VGA text attribute colors to ANSI colors.
#
VGA_TO_ANSI = (0, 4, 2, 6, 1, 5, 3, 7)

old_result = list()


def character2ansi_sequence((character, attribute)):
    """Converts the (character, attribute) tuple into a string with an
    ANSI sequence for the `attribute` followed by the `character`.
   
    `attribute` is a one byte string with a VGA text attribute.
    """
    global old_result
    result = list()
    #
    # Extract foreground and background color from attribute.
    #
    value = ord(attribute)
    foreground = value & 7
    background = (value >> 4) & 7
    #
    # reset bold text and emphasis
    #
    result.append(0)
    #
    # Check for the "bold" bit.
    #
    if value & 8:
        result.append(1)
    #
    # Check for the "emphasize" bit.
    #
    if value & 128:
        result.append(5)  # might have to be set to 4, according to terminal
    #
    # Add the colors.
    #
    result.append(VGA_TO_ANSI[foreground] + 30)
    result.append(VGA_TO_ANSI[background] + 40)
    if old_result == result:
    	return character
    else:
    	old_result = result
    	return '\033[%sm%s' % (';'.join(map(str, result)), character)


def iter_lines(fileobj):
    """Iterates over the lines of the screendump file.
   
    Returns an iterator over a list of (character, attribute) tuples per
    line of the screendump.  Both elements of the tuples are one byte strings.
    """
    line_length = struct.unpack('xB2x', fileobj.read(4))[0]
    line_size = line_length * 2
    for line in iter(lambda: fileobj.read(line_size), ''):
        yield zip(islice(line, 0, None, 2), islice(line, 1, None, 2))


def main():
    """Test code."""
    dump_file = open('test.dat', 'rb')
    for line in iter_lines(dump_file):
        print ''.join(imap(character2ansi_sequence, line))
    dump_file.close()


if __name__ == '__main__':
    main()

PS: gerade ist mir aufgefallen, dass bei VGA_TO_ANSI lediglich die Bits "1" und "4" vertauscht werden. Mit Bitoperationen lässt sich das nicht machen, oder?

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4638

Wohnort: Berlin

Iiiiiih global. Das ist schrecklich unsauber. ☺

Also nochmal ohne global mit einer Funktion, welche die Zeichen aus aufeinanderfolgenden Tupeln mit gleichem Attribut zusammenfasst:

#!/usr/bin/python 
# -*- coding: utf-8 -*- 
import struct
from itertools import groupby, imap, izip
from operator import itemgetter

# 
# Mapping from VGA text attribute colors to ANSI colors.
# 
VGA_TO_ANSI = (0, 4, 2, 6, 1, 5, 3, 7)


def character2ansi_sequence((characters, attribute)):
    """Converts the (characters, attribute) tuple into a string with an
    ANSI sequence for the `attribute` followed by the `characters`.
    
    `attribute` is a one byte string with a VGA text attribute.
    """
    # 
    # Start with the code to reset the ANSI attributes.
    # 
    result = ['0']
    # 
    # Extract foreground and background color from attribute.
    # 
    value = ord(attribute)
    # 
    # Check for the "bold" and "emphasize" bits.
    # 
    if value & 8:
        result.append('1')
    if value & 128:
        result.append('5')
    # 
    # Add the colors.
    # 
    foreground = value & 7
    background = (value >> 4) & 7
    result.append(str(VGA_TO_ANSI[foreground] + 30))
    result.append(str(VGA_TO_ANSI[background] + 40))
    
    return '\033[%sm%s' % (';'.join(result), characters)


def merge_on_attributes(character_data):
    """Merges characters with the same attributes.
    
    Takes an iterable of (characters, attribute) tuples and merges the
    characters of subsequent tuples with the same attributes.
    
    Returns an iterator over (characters, attribute) tuples.
    
    >>> a = [('a', 'x'), ('b', 'x'), ('c', 'y'), ('d', 'z'), ('e', 'z')]
    >>> list(merge_on_attributes(a))
    [('ab', 'x'), ('c', 'y'), ('de', 'z')]
    """
    for attribute, items in groupby(character_data, itemgetter(1)):
        yield (''.join(imap(itemgetter(0), items)), attribute)


def iter_lines(fileobj):
    """Iterates over the lines of the screendump file.
    
    Returns an iterator over iterators over (characters, attribute) tuples per
    line of the screendump.  The attribute is a one byte string that applies
    to all characters.
    """
    line_length = struct.unpack('xB2x', fileobj.read(4))[0]
    line_size = line_length * 2
    for line in iter(lambda: fileobj.read(line_size), ''):
        iterator = iter(line)
        yield merge_on_attributes(izip(iterator, iterator))


def main():
    """Test code."""
    dump_file = open('test.dat', 'rb')
    for line in iter_lines(dump_file):
        #print list(line)
        print ''.join(imap(character2ansi_sequence, line))
    dump_file.close()


if __name__ == '__main__':
    main()

Das vertauschen der Bits liesse sich wohl mit Bitoperationen umsetzen, allerdings würde das dann langsamer und komplizierter sein. Einen Geschwindigkeitsvorteil bringt das nur, wenn die Operationen nicht auf Variablem im Speicher, sondern auf Registern durchgeführt werden. Falls Du das Programm doch noch mal in Java, C oder ähnlichen Sprachen umsetzt, wäre das vielleicht eine Möglichkeit ein paar Mikrosekunden einzusparen. ☺

Pumbaa80 Team-Icon

(Themenstarter)
Avatar von Pumbaa80

Anmeldungsdatum:
5. März 2007

Beiträge: 2130

Wohnort: Residenz des Rechts

Ich glaube, auf eine Zeitersparnis im Mikrosekundenbereich kann ich ausnahmsweise verzichten 😉

Marc 'BlackJack' Rintsch hat geschrieben:

Iiiiiih global. Das ist schrecklich unsauber. ☺

Jaja, aber einfacher zu programmieren... *duck*

Jetzt will ich aber auch noch verraten, wofür ich dieses Skript brauche: Im Thread Pimp my Textkonsole kann man meine stümperhaften Versuche verfolgen, vernünftige ANSI-Art zu erstellen. Dabei hatte ich ganz übersehen, dass die libcaca das schon automatisch macht. Leider haben cacaview und Co. keine Funktion zum Speichern, deshalb muss man eben diesen kleinen Umweg gehen.

libcaca steht übrigens unter der Lizenz WTFPL - Do What The Fuck You Want To Public License 😲 😕 😀

Lunar

Anmeldungsdatum:
17. März 2006

Beiträge: 5792

Pumbaa80 hat geschrieben:

Ich glaube, auf eine Zeitersparnis im Mikrosekundenbereich kann ich ausnahmsweise verzichten 😉

Marc 'BlackJack' Rintsch hat geschrieben:

Iiiiiih global. Das ist schrecklich unsauber. ☺

Jaja, aber einfacher zu programmieren... *duck*

*hau* 😉

Pumbaa80 hat geschrieben:

libcaca steht übrigens unter der Lizenz WTFPL - Do What The Fuck You Want To Public License 😲 😕 😀

Ah, endlich mal eine Lizenz, die ich verstehe 😉 Ich werde sofort meine Code-Templates anpassen, MIT ist ja eigentlich viel zu kompliziert 😉

Pumbaa80 Team-Icon

(Themenstarter)
Avatar von Pumbaa80

Anmeldungsdatum:
5. März 2007

Beiträge: 2130

Wohnort: Residenz des Rechts

Marc 'BlackJack' Rintsch hat geschrieben:

Kleines Python-Programm:

Hatte mit meiner ATI-Karte schön funktioniert, aber mit meiner (neuen) nVidia-Karte waren die Farben verdreht. Vielleicht lags auch am Upgrade auf Gutsy, egal.
Kurze Analyse ergab, dass in /dev/vcsaX die Bits nun in anderer Reihenfolge stehen. Abhilfe schafft diese Änderung der Zeilen 30-38:

    if value & 16:
        result.append('1')
    if value & 1:
        result.append('5')
    #
    # Add the colors.
    #
    foreground = (value >> 1) & 7
    background = value >> 5


Rest wie oben

(außerdem habe ich noch "test.dat" durch "sys.argv[1]" ersetzt, und ein "import sys" hinzugefügt, damit man den Dateinamen als Parameter eingeben kann.)

Antworten |