Base64-Scripts zusammenfassen

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
kyou
User
Beiträge: 50
Registriert: Sonntag 7. November 2010, 08:38

Hi,

eines vorab: Ich kenne mich mit Python nicht aus.

Ich habe mir gestern ein Base64-Script (b64.py -- GitHub) heruntergeladen und so modifiziert, dass es die Zeilen nach 64 bzw. 76 Zeichen umbricht. Auch meine urlsafe-Variante funktioniert. Allerdings habe ich jetzt sechs Skripte. Ich würde die gerne zusammenfassen, indem ich das ursprüngliche Skript um die Optionen "urlsafe" und "line length" erweitere. Das Skript muss unter Python 2.7 funktionieren, weil ich die Python-Version, die im Adobe Font Development Kit for OpenType enthalten ist, anspreche.

Wahrscheinlich bekäme ich das irgendwann auch selbst hin. Aber es würde Tage dauern. Vielleicht kann mir jemand dabei helfen.

Edit: Die Zeilenumbrüche sind wegen der UNIX-Kodierung in Notepad übrigens nicht sichtbar, in UltraEdit aber schon. Was ist denn die Windows- bzw. DOS-Entsprechung für "/n"?

Das ursprüngliche Skript:

Code: Alles auswählen

import sys
import argparse
import base64

parser = argparse.ArgumentParser(description='Encode and Decode base64.')

group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-e', '--encode', action='store_const', const=base64.standard_b64encode, dest='func')
group.add_argument('-d', '--decode', action='store_const', const=base64.standard_b64decode, dest='func')

parser.add_argument('-i', '--infile', dest='infile', metavar='INFILE', help='Defaults to stdin')
parser.add_argument('-o', '--outfile', dest='outfile', metavar='OUTFILE', help='Defaults to stdout')
parser.add_argument('args', nargs='*')

opts = parser.parse_args()

if (opts.infile == None):
    instr = ' '.join(opts.args).encode()
else:
    with open(opts.infile, 'rb') as ifh:
        instr = ifh.read()

outstr = opts.func(instr)

if (opts.outfile == None):
    print(outstr) 
else:
    with open(opts.outfile, 'wb') as ofh:
        ofh.write(outstr)
Meine urlsafe Variante:

Code: Alles auswählen

import sys
import argparse
import base64

parser = argparse.ArgumentParser(description='Encode and Decode base64.')

group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-e', '--encode', action='store_const', const=base64.urlsafe_b64encode, dest='func')
group.add_argument('-d', '--decode', action='store_const', const=base64.urlsafe_b64decode, dest='func')

parser.add_argument('-i', '--infile', dest='infile', metavar='INFILE', help='Defaults to stdin')
parser.add_argument('-o', '--outfile', dest='outfile', metavar='OUTFILE', help='Defaults to stdout')
parser.add_argument('args', nargs='*')

opts = parser.parse_args()

if (opts.infile == None):
    instr = ' '.join(opts.args).encode()
else:
    with open(opts.infile, 'rb') as ifh:
        instr = ifh.read()

outstr = opts.func(instr)

if (opts.outfile == None):
    print(outstr) 
else:
    with open(opts.outfile, 'wb') as ofh:
        ofh.write(outstr) 
Meine Zeilenumbruch-Variante:

Code: Alles auswählen

import sys
import argparse
import base64

parser = argparse.ArgumentParser(description='Encode and Decode base64.')

group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-e', '--encode', action='store_const', const=base64.standard_b64encode, dest='func')
group.add_argument('-d', '--decode', action='store_const', const=base64.standard_b64decode, dest='func')

parser.add_argument('-i', '--infile', dest='infile', metavar='INFILE', help='Defaults to stdin')
parser.add_argument('-o', '--outfile', dest='outfile', metavar='OUTFILE', help='Defaults to stdout')
parser.add_argument('args', nargs='*')

opts = parser.parse_args()

def insert_newlines(string, every=64):
    return '\n'.join(string[i:i+every] for i in xrange(0, len(string), every))

if (opts.infile == None):
    instr = ' '.join(opts.args).encode()
else:
    with open(opts.infile, 'rb') as ifh:
        instr = ifh.read()

outstr = opts.func(instr)

if (opts.outfile == None):
    print(insert_newlines(outstr)) 
else:
    with open(opts.outfile, 'wb') as ofh:
        ofh.write(insert_newlines(outstr)) 
Sirius3
User
Beiträge: 18299
Registriert: Sonntag 21. Oktober 2012, 17:20

@kyou: wie man Optionen definiert, siehst Du ja schon an en-/decode. Wenn Du die Standard und URLSafe Varianten gleichzeitig bedienen willst, kannst Du natürlich store_const für --encode und --decode nicht mehr verwenden. Wenn Du Text-Dateien mit Betriebssystemabhängigem Zeilenumbruch erzeugen willst, solltest Du sie halt nicht als Binärdateien öffnen; damit unterscheidet sich natürlich die Dateiausgabe für En- und Decodieren.

Auf None prüft man üblicherweise mit is und nicht mit == und die Klammern um die Bedingungen sind überflüssig. Wenn Du Python 2.7 verwendest, solltest Du print_function aus dem __future__-Modul importieren, damit print auch wirklich eine Funktion ist, und nicht nur wie eine geschrieben wurde.
kyou
User
Beiträge: 50
Registriert: Sonntag 7. November 2010, 08:38

wie man Optionen definiert, siehst Du ja schon an en-/decode. Wenn Du die Standard und URLSafe Varianten gleichzeitig bedienen willst, kannst Du natürlich store_const für --encode und --decode nicht mehr verwenden. Wenn Du Text-Dateien mit Betriebssystemabhängigem Zeilenumbruch erzeugen willst, solltest Du sie halt nicht als Binärdateien öffnen; damit unterscheidet sich natürlich die Dateiausgabe für En- und Decodieren.
Das ursprüngliche Script stammt, wie gesagt, nicht von mir. Die Dateien, die ich öffne sind Binärdateien (Fonts und Bilder). Wie öffne ich die denn nicht als Binärdateien?

Sorry, normalerweise versuche ich es immer erst mal selbst und google. Habe ich auch in diesem Fall gemacht und einen Großteil des gestrigen Tages damit verbracht. Aber irgendwann kommt halt manchmal der Punkt, an dem man merkt, dass man ohne Basiswissen nicht weiterkommt. Und dann muss man abwägen, ob man das Basiswissen in Zukunft oft genug brauchen wird, um Zeit in den Erwerb dieses Wissens zu investieren. In diesem Fall stünde das in keiner günstigen Relation. Und deswegen hoffe ich darauf, dass jemand ein Base64-Script, wie ich es hier angedacht habe, nützlich genug findet, um das Problem mit seinem schon vorhandenen Basiswissen zu lösen, und das Ergebnis mir und anderen zu Verfügung zu stellen. Ich erwarte also nicht, dass sich jemand nur wegen mir und ohne Bezahlung viel Arbeit macht.

Die sechs Skripte erfüllen ja auch, so wie sie sind, ihren Zweck. Ich fände es halt nur schöner, wenn sie zusammengefasst wären. Ist also für mich neben dem grundsätzlichen Interesse ein ästhetisches Problem. Ist halt unschön, sechs Skripte zu haben, die im Wesentlichen gleich sind. :D
Auf None prüft man üblicherweise mit is und nicht mit == und die Klammern um die Bedingungen sind überflüssig. Wenn Du Python 2.7 verwendest, solltest Du print_function aus dem __future__-Modul importieren, damit print auch wirklich eine Funktion ist, und nicht nur wie eine geschrieben wurde.
Mmh. Das Script funktioniert aber unter Python 2.7 auch ohne "from __future__ import print_function". Warum genau sollte ich "print_function" importieren?

Aber danke! Ich habe das geändert:

Code: Alles auswählen

from __future__ import print_function
import sys
import argparse
import base64

parser = argparse.ArgumentParser(description='Encode and Decode base64.')

group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-e', '--encode', action='store_const', const=base64.standard_b64encode, dest='func')
group.add_argument('-d', '--decode', action='store_const', const=base64.standard_b64decode, dest='func')

parser.add_argument('-i', '--infile', dest='infile', metavar='INFILE', help='Defaults to stdin')
parser.add_argument('-o', '--outfile', dest='outfile', metavar='OUTFILE', help='Defaults to stdout')
parser.add_argument('args', nargs='*')

opts = parser.parse_args()

if opts.infile is None:
    instr = ' '.join(opts.args).encode()
else:
    with open(opts.infile, 'rb') as ifh:
        instr = ifh.read()

outstr = opts.func(instr)

if opts.outfile is None:
    print(outstr) 
else:
    with open(opts.outfile, 'wb') as ofh:
        ofh.write(outstr) 
kyou
User
Beiträge: 50
Registriert: Sonntag 7. November 2010, 08:38

Das Problem mit dem Umbruchszeichen war leicht zu lösen: Ich musste nur "\n" durch "\r\n" ersetzen. Das hatte eben aus irgend einem Grund nicht geklappt. Aber ich habe es gerade nochmal versucht.

Schöner wäre es aber, wenn das Script sich in Bezug auf die Umbruchszeichen nach dem System richtete, auf dem es läuft.
Sirius3
User
Beiträge: 18299
Registriert: Sonntag 21. Oktober 2012, 17:20

@kyou: hier die paar kleine Änderungen...

Code: Alles auswählen

from __future__ import print_function
import argparse
import base64
 
ENCODERS = [
    (base64.standard_b64encode, base64.standard_b64decode),
    (base64.urlsafe_b64encode, base64.urlsafe_b64decode),
]
 
def parse_args(args=None):
    parser = argparse.ArgumentParser(description='Encode and Decode base64.')
    
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument('-e', '--encode', action='store_false', dest='decode')
    group.add_argument('-d', '--decode', action='store_true')
    
    parser.add_argument('--urlsafe', action='store_true', help='Encode urlsafe')
    parser.add_argument('--line-length', help='Defaults to unlimited', default=0, type=int)

    parser.add_argument('-i', '--infile', help='Defaults to stdin')
    parser.add_argument('-o', '--outfile', help='Defaults to stdout')
    parser.add_argument('args', nargs='*')
    
    return parser.parse_args(args)

def insert_newlines(string, every=64):
    return '\n'.join(string[i:i+every] for i in xrange(0, len(string), every))

def get_input(opts):
    if opts.infile is None:
        instr = ' '.join(opts.args).encode()
    else:
        with open(opts.infile, 'r' if opts.decode else 'rb') as ifh:
            instr = ifh.read()
    return instr
    
def write_output(opts, outstr):
    if opts.outfile is None:
        print(outstr)
    else:
        with open(opts.outfile, 'wb' if opts.decode else 'w') as ofh:
            ofh.write(outstr)

def main():
    opts = parse_args()
    instr = get_input(opts)
    outstr = ENCODERS[opts.urlsafe][opts.decode](instr)
    if opts.line_length and opts.encode:
        outstr = insert_newlines(outstr, opts.line_length)
    write_output(opts, outstr)

if __name__ == '__main__':
    main()
kyou
User
Beiträge: 50
Registriert: Sonntag 7. November 2010, 08:38

Danke sehr, Sirius3!

Die URL-Safe-Option funktioniert.

Bei der Line-Length-Option erhalte ich folgende Fehlermeldung:

Code: Alles auswählen

Traceback (most recent call last):
  File "b64.py", line 53, in <module>
    main()
  File "b64.py", line 48, in main
    if opts.line_length and opts.encode:
AttributeError: 'Namespace' object has no attribute 'encode'
(Die Dekodier-Option habe ich noch nicht getestet.)
Sirius3
User
Beiträge: 18299
Registriert: Sonntag 21. Oktober 2012, 17:20

»opts.encode« sollte auch »not opts.decode« heißen.
kyou
User
Beiträge: 50
Registriert: Sonntag 7. November 2010, 08:38

Klasse. Funktioniert soweit. Urlsafe Code lässt sich nur mit der Option --urlsafe dekodieren. Aber das kann ich im Batch per Findstr (?) vorher testen.
kyou
User
Beiträge: 50
Registriert: Sonntag 7. November 2010, 08:38

Wäre es okay für dich, wenn ich auf GitHub vorschlüge, dass dein Script mit ins AFDKO (Adobe Font Development Kit for OpenType) aufgenommen wird? Da auch die FontTools mit Subset-Script im AFDKO integriert sind (wenn man den Build Notes folgt), hätte man dann alles, was man braucht, um Fonts fürs Web vorzubereiten, in einem Paket und wäre unabhängig von Online-Webfontgeneratoren wie dem von Fontsquirrel.
Sirius3
User
Beiträge: 18299
Registriert: Sonntag 21. Oktober 2012, 17:20

das ursprüngliche Skript stand unter GNU General Public License V2. Auch wenn die Schöpfungshöhe nicht sehr hoch ist geht man doch auf der sicheren Seite, wenn ich das hier veröffentlichte Skript auch unter GNU General Public License V2 stelle. Damit ist es unter den gegebenen Bedingungen frei verwendbar.
Benutzeravatar
snafu
User
Beiträge: 6881
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

IMHO sollte das Skript bei Angabe zur Zeilenlänge gegen unerwartetes Verhalten abgesichert werden. Denn wenn dort jemand aus Versehen eine negative Angabe macht, dann spuckt `insert_newlines()` einen leeren String aus. Daher sollte man im Falle einer negativen Zeilenlänge entweder eine Fehlermeldung ausgeben oder in `main()` die Zeile 48 durch ``if opts.line_length > 0 and opts.encode:`` ersetzen. Bei letzterem würden negative Zeilenlängen einfach wie 0 (d.h. in diesem Kontext: unendlich) behandelt werden. Ich würde mich wahrscheinlich für die Fehlermeldung entscheiden.
kyou
User
Beiträge: 50
Registriert: Sonntag 7. November 2010, 08:38

@ Sirius3

Cool.

@ snafu

Naja, wenn du wüßtest, wie viele Bugs sich noch im AFDKO tummeln, die ihre Fühler nach Usern ausstrecken, die keine ungültigen Angaben bei den Optionen machen …

Jedenfalls müsste das Script dann auch dagegen abgesichert sein, dass jemand versucht eine urlsafe codierte Datei im Normalmodus zu dekodieren. Und umgekehrt. Aber meiner Meinung nach lässt sich das AFDKO eh nur mit Hilfe von Batch-Dateien einigermaßen komfortabel bedienen. Und da schreibt man die Optionen ja nur einmal rein.

Im Grunde sollten auch alle Zahlenwerte ungleich 64 und 76 ungültig sein. Außer unendlich. Aber das ist ja keine Zahl. Oder gibt es irgend eine Software, die Base64 mit einer Zeilenlänge ungleich 64, 76 oder unendlich versteht?

Aber da du schon eine Lösung für Zeile 48 hast, ersetze ich die Zeile und schlag das Script dann für das AFDKO vor. Änderungen sind dann ja trotzdem jederzeit möglich. Das gilt ja auch für sämtliche andere im AFDKO enthaltene Dateien.

Übrigens: Falls ihr euch für das AFDKO interessiert: Leider arbeiten da nicht allzu viele Leute dran. Wenn ihr daran mitwirken würdet, wäre das sicher eine Bereicherung. Dafür muss man sich aber schon sehr für Fonts interessieren. Und Zeit haben. :)

Auf jeden Fall ist es ein ziemlich geniales Paket, um Fonts zu dekompilieren, zu generieren, zu manipulieren u. s. w. Auch dank der Fonttools.
Benutzeravatar
snafu
User
Beiträge: 6881
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

kyou hat geschrieben:Im Grunde sollten auch alle Zahlenwerte ungleich 64 und 76 ungültig sein. Außer unendlich. Aber das ist ja keine Zahl. Oder gibt es irgend eine Software, die Base64 mit einer Zeilenlänge ungleich 64, 76 oder unendlich versteht?
Wikipedia sagt dazu, dass es anscheinend egal ist:
In der Darstellung von sehr langen Base64-Strings werden diese oftmals (zum Beispiel nach jeweils 64 Zeichen) umgebrochen, also ein Zeilenumbruch eingefügt. Solche Zeilenumbrüche sind für die Kodierung nicht von Belang und werden ignoriert.
Quelle: https://de.wikipedia.org/wiki/Base64#Vo ... _Kodierung

Ich bin aber kein Experte für Base64. Mir ist es nur ins Auge gesprungen. Solche Fehler sind halt meistens dann fies, wenn die Angabe nicht direkt gemacht wird, sondern aus einer Variable gezogen oder berechnet wird. Dann dauert es unter Umständen eine "halbe Ewigkeit" bis man den Grund findet, warum der Befehl bzw das Skript keinen Output erzeugt.
kyou
User
Beiträge: 50
Registriert: Sonntag 7. November 2010, 08:38

Solche Zeilenumbrüche sind für die Kodierung nicht von Belang und werden ignoriert.
Schön wär's. Irgendwie gibt es da aber immer wieder Überraschungen. Ich habe gerade in ein CSS einen base64 codierten Font mit Zeilenumbrüchen und ein base64 codiertes Hintergrundbild mit Zeilenumbrüchen eingebettet. Zeilenlänge in beiden Fällen 64 Zeichen. Der Font wird angezeigt, aber das Hintergrundbild nicht. Lösche ich die Umbrüche, wird es angezeigt.

Ich kann's nicht beschwören, bin mir aber recht sicher, dass ich gestern einen base64-codierten Font eingebettet habe mit ebenfalls begrenzter Zeilenlänge (64 oder 76 Zeichen -- weiß ich nicht mehr). Und der wurde nicht angezeigt. Erst als ich die Umbrüche gelöscht habe.

Edit: Ohne Zeilenumbrüche ist man jedenfalls auf der sicheren Seite. Mit einem anderen Font, der base64-codiert war, mit begrenzter Zeilenlänge, hat es gerade nämlich schon wieder nicht funktioniert. Mir fällt auf, dass der Base64 Code manchmal mit einem, manchmal mit zweien und manchmal ohne Gleichheitszeichen abschließt. Vielleicht gibt es da einen Zusammenhang.
BlackJack

@snafu: Wikipedia sagt recht allgemein das Zeilenumbrüche egal sind, hat als Quelle aber RFC 4648 verlinkt, was erst einmal sagt das Zeilenumbrüche verboten sind, sofern die Spezifikation, die sich auf RFC 4648 abstützt, nichts anderes sagt. Und da zählt dann was eben diese andere Spezifikation sagt. Die kann sagen „ist egal“, oder die kann auch feste Zeilenlängen oder maximale Zeilenlängen vorgeben. Die 76 Zeichen kommen zum Beispiel von der MIME-Spezifikation. Dort dürfen Zeilen nicht länger sein, dass heisst da muss man auch Base64 nach 76 Zeichen umbrechen, oder kann nur sehr kleine Anhänge verschicken. ;-) Und MIME sagt auch das alles was nicht zum Alphabet gehört, einfach ignoriert werden kann.

Für andere Spezifikationen können andere Regeln bezüglich Zeilenumbrüchen im speziellen, und Zeichen ausserhalb des gewählten Base64-Alphabet im allgemeinen gelten. Bei HTML oder CSS würde ich beispielsweise mal vermuten, dass bei Data-URLs keine Zeilenumbrüche erlaubt sind, weil die in URLs nicht erlaubt sind.
kyou
User
Beiträge: 50
Registriert: Sonntag 7. November 2010, 08:38

Für andere Spezifikationen können andere Regeln bezüglich Zeilenumbrüchen im speziellen, und Zeichen ausserhalb des gewählten Base64-Alphabet im allgemeinen gelten. Bei HTML oder CSS würde ich beispielsweise mal vermuten, dass bei Data-URLs keine Zeilenumbrüche erlaubt sind, weil die in URLs nicht erlaubt sind.
Aber es funktioniert eben manchmal.

Es gibt aber anscheinend eine Lösung, nämlich die Data-URL in Anführungszeichen zu setzen und den Umbruch mit Backslash einzuleiten:
Siehe stackoverflow.

Dann ist es nur eigentlich kein Base64 mehr.

--------------------------------

Fehlt also noch die Option, vor dem Zeilenumbruch ein Backslash einzufügen. Geht das mit einer Variable vor '\n', die mit einer Option "--end-lines-with-backslash" auf "\\" gesetzt wird?
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Naja, die Data URL hat ja auch eine Syntax die irgendwie interpretiert wird, nach diesen Regeln kann da durchaus dann noch korrektes base64 heraus kommen. Außerdem kann es durchaus auch mal Unterschiede geben zwischen dem was die Spezifikation sagt und was in der Praxis getan wird, vorallem wenn die Spezifikation nicht eindeutig irgendwas erzwingt.
BlackJack

@kyou: Doch das ist dann noch sowohl Base64 als auch eine URL, denn da sind gar keine Backslashes und keine Zeilenumbrüche enthalten. Die sind in dem CSS-Zeichenkettenliteral und da ist offenbar spezifiziert das ein \ gefolgt von einem Zeilenumbruch vom CSS-Parser ignoriert wird, also nach dem parsen einfach nicht mehr da ist.

Edit: Vielleicht wäre es sinnvoll eine Option anzubieten die gleich eine Data-URL erzeugt.
kyou
User
Beiträge: 50
Registriert: Sonntag 7. November 2010, 08:38

Vielleicht wäre es sinnvoll eine Option anzubieten die gleich eine Data-URL erzeugt.
Mit dem richtigen MIME-Type. Bei einigen Fonttypen scheint nicht abschließend geklärt zu sein, was für ein MIME-Type sie sind. Aber eine Liste mit den MIME-Types im Script oder einer Extra-Datei, die das Script ausliest, könnte ja der User leicht ändern bzw. ergänzen.

Im Moment steht in meiner Batch-Datei folgendes:

Dateiendung otf: MIME-Type: font/opentype; format (opentype)
Dateiendung ttf: MIME-Type: font/truetype; format (truetype)
Dateiendung woff: MIME-Type: application/font-woff; format (woff)
Dateiendung woff2: MIME-Type: application/font-woff2; format (woff2)
Dateiendung eot: MIME-Type: application/vnd.ms-fontobject; format (eot)

Genauer gesagt stehen die getrennt in For-Loops:

[codebox=bash file=Unbenannt.bsh]@echo off
Setlocal EnableDelayedExpansion

for %%f in (Input\*.ttf) do (
echo @font-face {>"Output\%%~nxf.txt"
echo font-family: '%%~nf';>>"Output\%%~nxf.txt"
set /p =src: url^(data:font/truetype;charset=utf-8;base64,<NUL>>"Output\%%~nxf.txt"
%python_exe% b64.py -e -i "%%f">>"Output\%%~nxf.txt"
echo ^) format^('truetype'^);>>"Output\%%~nxf.txt"
echo font-weight: normal;>>"Output\%%~nxf.txt"
echo font-style: normal;>>"Output\%%~nxf.txt"
echo }>>"Output\%%~nxf.txt"
)[/code]

Die Batch-Datei packt den Base64-Code dann auch noch in CSS-Deklarationsblöcke. Eine CSS-Deklarationsblock-Option würde ich aber eher nicht im Python-Script anbieten. Ich denk, die ist in einer Batch-Datei besser aufgehoben.
Antworten