Pdf kodowany w Delphi do Base64 i dekodowany w JavaScript Native messaging

0

host = program lokalny EXE, host Native messaging

Potrzebuję przesłać pdf z hosta na serwer jako plik pdf.

Pomyślałem, że zakoduję PDF do BASE64 i wyślę w częściach (ograniczenie do 1MB)

NativeMessaging to sposób na obejście problemu braku dostępu javascriptu do lokalnych plików.
Google wymyśliło system komunikatów.
https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging?hl=pl

W skrócie działa to tak:
Javasript strony(tej którą widzimy) wysyła wiadomości do skryptu działającego w tle(strona go nie kontroluje)
Skrypt przesyła wiadomości do hosta

host wysyła do skryptu
skrypt odsyła do strony (interfejs użytkownika)

Całość odbywa się poprzez strumienie stdin i stdout, skryptu i hosta
Wiadomości są przesyłane Jako JSON kodowany do UTF8.

Całość opracowałem i działa dla zwykłych danych tekstowych (tz nie widzę błędów).
Niestety nie działa dla danych binarnych

Wysyłanie komunikatów po stronie Delphi


var utf8: UTF8String;
begin
...
reply := UTF8Encode(Format('{"Wysyłam dane ": "%s" }', ['Plik pdf.']));
WriteMessage(reply);
...



        len:=Length(co);
        while i < len do
        begin
          fra:=Copy(co, i, lensend);
          reply := UTF8Encode(Format('{"POL": "dane","part":"%s","data":"%s" }', [IntToStr(j),fra]));
          WriteMessage(reply);
          Memo1.Lines.Add('reply: ' + reply);
          inc(i, lensend);
          inc(j);
          Application.ProcessMessages;
        end;
        reply := UTF8Encode(Format('{"POL": "%s" }', ['Transfer_complite']));
        WriteMessage(reply);



Skrypt pośredniczący zajmuje się połączeniami i tylko przekazywaniem wiadomości w obie strony

Pod spodem łączenie danych otrzymanych z hosta w Javasript
Oczywiście dekodowanie daje błąd.


port.onMessage.addListener(function(msg) {
...
if (msg.POL=="dane") {
  			plikbase64array+=msg.data;  		
  		}
if (msg.POL=="Transfer_complite") {
var encodedString = atob(plikbase64array);



0

Chyba wiem gdzie jest problem.
Nie wszystkie transfery dochodzą

to jest fragment transmisji

reply: {"POL": "dane","part":"0","data":"JVBERi0xLjQKJcfsj6IKJSVJb" }
reply: {"POL": "dane","part":"1","data":"nZvY2F0aW9uOiBwYXRoL2dzd2" }
reply: {"POL": "dane","part":"2","data":"luMzJjLmV4ZSAtZERpc3BsYXl" }
reply: {"POL": "dane","part":"3","data":"G
b3JtYXQ9MTk4Nzg4IC1kRG" }

Ostatni transfer nie dochodzi (pewnie chodzi o liniebrek)
i dane są niekompletne.

Myślę nad innym sposobem transferu, pewnie każdy znak trzeba wysłać jako reprezentację liczbową.

0
karol75 napisał(a):

Myślę nad innym sposobem transferu, pewnie każdy znak trzeba wysłać jako reprezentację liczbową.

Spróbuj escapować znaki kontrolne — na pewno znajdziesz do tego funkcje w bibliotece Delphi. Poza tym, w podanych snippetach nie widzę, abyś cokolwiek konwertował do i z BASE64.

0

Czy tworzenie pdf nie jest oparte o strumien, ktory nalezy zakończyc?

0
pstmax napisał(a):

Czy tworzenie pdf nie jest oparte o strumien, ktory nalezy zakończyc?

Nie, plik jest tworzony przez lite pdf.
Po przesłaniu widzę części pdf-a ale nie wszystkie.

Funkcja enkodująca:

function EncodeFile(const filename: string): AnsiString;
var
  stream: TMemoryStream;
begin
  stream := TMemoryStream.Create;
  try
    stream.LoadFromFile(filename);
    Result := EncodeBase64(stream.Memory, stream.Size);
  finally
    stream.Free;
  end;
end;

wysyłanie wiadomosci:

form:=Format('{"POL": "dane","part":"%s","data":"%s" }', [IntToStr(j), ca]);
      reply := UTF8Encode(form);
      WriteMessage(reply);
class procedure TSkanowanie.WriteMessage(const msg: UTF8String);
begin
  StdOut.WriteData(Length(msg));
  StdOut.Write(PUTF8Char(msg)^, Length(msg));
end;
0

A czym jest ca i jakie posiada dane w trakcie wywołania poniższej linijki?

form:=Format('{"POL": "dane","part":"%s","data":"%s" }', [IntToStr(j), ca]);
0
furious programming napisał(a):

A czym jest ca i jakie posiada dane w trakcie wywołania poniższej linijki?

form:=Format('{"POL": "dane","part":"%s","data":"%s" }', [IntToStr(j), ca]);

Kompletna funkcja


function MyEncodeUrl(source:string):string;
 var i:integer;
 begin
   result := '';
   for i := 1 to length(source) do
   result := result + {'%'+}inttohex(ord(source[i]),2);
 end;


procedure TSkanowanie.AZapiszExecute(Sender: TObject);
var
  lensend, len: Integer;
  i, j: Integer;
  reply: UTF8String;
  utf8: UTF8String;
  co,ca,form: string;
  ASFile : AnsiString;

begin
  lensend := SpinEdit1.Value;

  if fileexists(filename) then
  begin
    ScanStatus := 'Send_SCAN';
    reply := UTF8Encode(Format('{"Wysyłam dane ": "%s" }', ['Plik pdf.']));
    WriteMessage(reply);
    WebBrowser1.Navigate2(filename);
    ASFile:= EncodeFile(filename);
    Memo1.Lines.Add(ASFile);
    len:=Length(ASFile);
    j:=1;
    while i < len do
    begin
      co:=copy(ASFile,i,lensend);
      ca:=MyEncodeUrl(co); //to próba obejścia problemu

      form:=Format('{"POL": "dane","part":"%s","data":"%s" }', [IntToStr(j), ca]);
      reply := UTF8Encode(form);
      WriteMessage(reply);
      Memo1.Lines.Add('reply: ' + reply);

      inc(i, lensend);
      inc(j);
    end;
    reply := UTF8Encode(Format('{"POL": "%s" }', ['Transfer_complite']));
    WriteMessage(reply);
  end;
end;

0

Chaos w tym kodzie — trudno cokolwiek zrozumieć. 😕

Z tego co jednak zrozumiałem, jedyną rzeczą, którą należy zakodować w Base64 jest fragment pliku, a reszta to czyste ASCII. Skoro tak, to otwórz plik, w pętli wczytuj fragment, koduj go w Base64, a następnie złącz z nagłówkiem (z tym co masz w funkcji Format). Nie powinieneś kodować wszystkiego (czyli całej wiadomości), a tylko dane wczytane z pliku, bo tylko one mogą zawierać znaki kontrolne.

Ostatecznie każda wiadomość powinna wyglądać tak:

{"POL": "dane","part":"1","data":"tu_fragment_pliku_zakodowany_w_base64" }

Czyli zmienna ca powinna zawierać fragment pliku zakodowany w Base64, zanim użyjesz jej w funkcji Format, produkującej powyższą linijkę z wiadomością.


Przy okazji — funkcja Format obsługuje mnóstwo typów danych (oraz różne style formatowania parametrów), więc zamiast ręcznie konwertować liczby do stringów, po prostu skorzystaj z maski %d dla intów:

form:=Format('{"POL": "dane","part":"%d","data":"%s" }', [j, ca]);
0
furious programming napisał(a):

Chaos w tym kodzie — trudno cokolwiek zrozumieć. 😕

Z tego co jednak zrozumiałem, jedyną rzeczą, którą należy zakodować w Base64 jest fragment pliku

Nie, źle zrozumiałeś.

Aby przesłać dane do przeglądarki używam Native Messaging, które to komunikują się za pomocą strumieni IN i Out
Strona <=> skrypt <=> host (skrypt w moim przypadku to pośrednik)

host => mój program
Każdy to oddzielna instancja.

Strona wysyła do skryptu wiadomość
skrypt odbiera i przesyła do hosta

host wysyła do skryptu wiadomość
skrypt przesyła tą wiadomość do strony.

Cała komunikacja musi być kodowana w UTF8 jako JSON

Przesłać muszę plik PDF.

form:=Format('{"POL": "dane","part":"%s","data":"%s" }', [IntToStr(j), ca]);
      reply := UTF8Encode(form);
      WriteMessage(reply);

To jest owijka kodująca do UTF8 i fizycznie wysyłająca JSON w UTF8 do swojego strumienia OUT (konsola) skrypt czyta dane i przesyła do strony.

Native Messaging narzuca ograniczenia:
UTF8
JSON
do 1MB wielkość całego JSON

Z obserwacji:
Jeżeli moich danych jest więcej niż 25-30 Bajtów to przeglądarka (strona -> console.log) głupieje, w ogóle ich nie pokazuje co nie znaczy że nie docierają.

1
karol75 napisał(a):
furious programming napisał(a):

Chaos w tym kodzie — trudno cokolwiek zrozumieć. 😕

Z tego co jednak zrozumiałem, jedyną rzeczą, którą należy zakodować w Base64 jest fragment pliku

Nie, źle zrozumiałeś.

Dobrze rozumiem ten temat. Jedyne co powinno być zakodowane w Base64 to dane, które są fragmentem pliku PDF. Finalny ciąg znaków powinien być w formacie JSON, zakodowany w UTF-8. A biorąc pod uwagę to, że JSON w swojej składni wykorzystuje jedynie znaki ASCII (tak samo jak Base64), spokojnie mogę pisać, że cały payload ma być ciągiem znaków ASCII.


Tak więc jeszcze raz — w pętli wczytaj fragment pliku PDF jako tablicę bajtów (surowe dane ze strumienia plikowego), następnie przekonwertuj ten fragment na ciąg znaków używając kodera Base64, potem połącz wynik z szablonem JSON, a na koniec przekonwertuj cały wynikowy ciąg znaków na UTF-8 i wyślij. W ten sposób będziesz miał pakiety danych zgodne z formatem JSON, bez znaków kontrolnych, mogących zepsuć składnię pliku, a więc uniemożliwić odczyt danych po stronie ”odbiornika" (klienta).

Natomiast aby poprawnie odczytać te dane w trakcie ich odbioru, wystarczy odczytać ciąg znaków z pola data (za pomocą API do formatu JSON) i przekonwertować go na ciąg bajtów (czyli blok bajtów będący fragmentem pliku PDF). Taki ciąg będziesz mógł wpisać do nowego pliku (a dla testu, wypisać w konsoli).


Nie wiem czy te biblioteki zapewniają odpowiednią kolejność dostarczania pakietów, więc dobrze by było najpierw wysłać pakiet z danymi określającymi początek transmisji (ramka z docelową liczbą pakietów, rozmiarem danych każdego z fragmentów itp.), a dopiero potem wysyłać pakiety jeden po drugim. Każdy pakiet powinien zawierać swoje ID oraz dane właściwe (fragment PDF).

W ten sposób nie tylko odbiorca będzie wiedział ilu pakietów ma się spodziewać, ale też będzie mógł stworzyć plik z docelowym rozmiarem i zapisywać odebrane dane w odpowiednich miejscach strumienia, nawet jeśli pakiety przylecą w złej kolejności. Ale tak jak pisałem — jeśli kolejność pakietów jest zapewniona, to mniej roboty będzie z zapisem danych do pliku.

0
furious programming napisał(a):
karol75 napisał(a):
furious programming napisał(a):

Chaos w tym kodzie — trudno cokolwiek zrozumieć. 😕

Z tego co jednak zrozumiałem, jedyną rzeczą, którą należy zakodować w Base64 jest fragment pliku

Nie, źle zrozumiałeś.

Dobrze rozumiem ten temat. Jedyne co powinno być zakodowane w Base64 to dane, które są fragmentem pliku PDF. Finalny ciąg znaków powinien być w formacie JSON, zakodowany w UTF-8. A biorąc pod uwagę to, że JSON w swojej składni wykorzystuje jedynie znaki ASCII (tak samo jak Base64), spokojnie mogę pisać, że cały payload ma być ciągiem znaków ASCII.

Tak więc jeszcze raz — w pętli wczytaj fragment pliku PDF jako tablicę bajtów (surowe dane), następnie przekonwertuj ten fragment na ciąg znaków używając kodera Base64, potem połącz wynik z szablonem JSON, a na koniec przekonwertuj cały wynikowy ciąg znaków na UTF-8 i wyślij. W ten sposób będziesz miał pakiety danych zgodne z formatem JSON, bez znaków kontrolnych, mogących zepsuć składnię pliku, a więc uniemożliwić odczyt danych po stronie ”odbiornika" (klienta).

Natomiast aby poprawnie odczytać te dane w trakcie ich odbioru, wystarczy odczytać ciąg znaków z pola data (za pomocą API do formatu JSON) i przekonwertować go na ciąg bajtów (czyli blok bajtów będący fragmentem pliku PDF). Taki ciąg będziesz mógł wpisać do nowego pliku (a dla testu, wypisać w konsoli).

No i to właśnie nie działa.

reply: {"POL": "dane","part":"3","data":"G
b3JtYXQ9MTk4Nzg4IC1kRG" }

To jest zrobione tak jak opisujesz i ten "pakiet" nie dotarł.
Niezależnie czy:
najpierw cały plik do base64 i fragmentami przesyłać
wpierw wczytać tablicę bajtów zakodować i przesłać
giną pakiety z powodu końców linii.

Gdzieś któraś funkcja dorzuca koniec linii.

0

To co opisałem nie ma prawa nie działać, bo zapewnia odpowiedni format danych w odpowiednim kodowaniu. Jedyne co stoi na przeszkodzie to co najwyżej ograniczenia tej biblioteki — czyli np. limit rozmiaru pakietu, o którym pisałeś.

karol75 napisał(a):

Gdzieś któraś funkcja dorzuca koniec linii.

No to użyj debuggera i ją znajdź. 😉

0

Przerobiona funkcja.

procedure TSkanowanie.SendPdfToOutStream(lensend: Integer);
var
  plik: TMemoryStream;
  bajty: TBytes;
  ebajty: string;
  i, j, len: Integer;
  form: string;
  reply: UTF8String;
begin
  if fileexists(filename) then
  begin
    SetLength(bajty, lensend);
    plik := TMemoryStream.Create();
    try
      plik.LoadFromFile(filename);
      j := 1;
      while i < plik.size do
      begin
        ZeroMemory(bajty,lensend);
        len := plik.Read(bajty, lensend);
        ebajty := TNetEncoding.Base64.EncodeBytesToString(bajty);
        form := Format('{"POL": "dane","part":"%s","data":"%s" }', [IntToStr(j), ebajty]);
        reply := UTF8Encode(form);
        WriteMessage(reply);
        Memo1.Lines.Add('reply: ' + reply);
        inc(i, lensend);
        inc(j);
      end;
    finally
      plik.Free;
    end;
  end;
end;

image

Po prawej oryginał
Po lewej przesłany, złożony i pobrany z powrotem
Dane tekstowe w porządku binarne błędne.

Po stronie Javascript
składanie i dekodowanie Base64

Fragment konsoli z początkiem danych binarnych z PDF

testa:72 Odebrałem wiadomość
testa:72 {POL: 'dane', part: '18', data: 'ago1IDAgb2JqCjw8L0xlbmd0aCAxMjM1Lw=='}POL: "dane"data: "ago1IDAgb2JqCjw8L0xlbmd0aCAxMjM1Lw=="part: "18"[[Prototype]]: Object
testa:83 msg.data
testa:84 ago1IDAgb2JqCjw8L0xlbmd0aCAxMjM1Lw==
testa:85 j
5 0 obj
<</Length 1235/
testa:72 Odebrałem wiadomość
testa:72 {POL: 'dane', part: '19', data: 'RmlsdGVyL0ZsYXRlRGVjb2RlPj4Kc3RyZQ=='}
testa:83 msg.data
testa:84 RmlsdGVyL0ZsYXRlRGVjb2RlPj4Kc3RyZQ==
testa:85 Filter/FlateDecode>>
stre
testa:72 Odebrałem wiadomość
testa:72 {POL: 'dane', part: '20', data: 'YW0KeNqtV9tO40gQffdX1OOstHS62912mw=='}
testa:83 msg.data
testa:84 YW0KeNqtV9tO40gQffdX1OOstHS62912mw==
testa:85 am
xÚ­WÛNãH}÷WÔ㬴tºÛÝv
testa:72 Odebrałem wiadomość
testa:72 {POL: 'dane', part: '21', data: 'WY0EAmZZtBKQzA7Sah96YpMxSWxwHFnxbw=='}
testa:83 msg.data
testa:84 WY0EAmZZtBKQzA7Sah96YpMxSWxwHFnxbw==
testa:85 YfY´ÌÒjzb1IlpYño
testa:72 Odebrałem wiadomość
testa:72 {POL: 'dane', part: '22', data: '8wNb7buTOAQ0IBQFl+vU5Zzq6hcrshiY3w=='}
testa:83 msg.data
testa:84 8wNb7buTOAQ0IBQFl+vU5Zzq6hcrshiY3w==
testa:85 ó[í»84 ëÔåêê+²ß

PDF jest wyświetlany - biała strona

1

Kodujesz 100x nie wiem czy to konieczne ale OK nie ważne. Wątpię natomiast czy zawsze wielkość pliku jest podzielna przez lensend dlatego można by to kontrolować przez

len := plik.Read(bajty, lensend);
SetLength(bajty, len); //to dodać

Choć tak naprawdę nie wiem czy dopisanie czegoś na końcu dokumentu PDF go popsuje w każdym razie moja propozycja na pewno nie zaszkodzi jedynie odrobinę poprawi ten chaotyczny kod.

1

Druga linijka logu, czyli ta:

testa:72 {POL: 'dane', part: '18', data: 'ago1IDAgb2JqCjw8L0xlbmd0aCAxMjM1Lw=='}POL: "dane"data: "ago1IDAgb2JqCjw8L0xlbmd0aCAxMjM1Lw=="part: "18"[[Prototype]]: Object

zdaje się nie być prawidłowym JSON-em — jakieś śmieci na końcu widać, w dodatku używasz ' zamiast ", nie mam pojęcia dlaczego.

Sugeruję zaorać cały ten kod i napisać go jeszcze raz, tym razem z głową i porządnie (nazewnictwo, formatowanie, podział na funkcje). Obecna forma nie dość, że chaotyczna, to w dodatku pełna bugów.

0
furious programming napisał(a):

Druga linijka logu, czyli ta:

testa:72 {POL: 'dane', part: '18', data: 'ago1IDAgb2JqCjw8L0xlbmd0aCAxMjM1Lw=='}POL: "dane"data: "ago1IDAgb2JqCjw8L0xlbmd0aCAxMjM1Lw=="part: "18"[[Prototype]]: Object

zdaje się nie być prawidłowym JSON-em — jakieś śmieci na końcu widać, w dodatku używasz ' zamiast ", nie mam pojęcia dlaczego.

Sugeruję zaorać cały ten kod i napisać go jeszcze raz, tym razem z głową i porządnie (nazewnictwo, formatowanie, podział na funkcje). Obecna forma nie dość, że chaotyczna, to w dodatku pełna bugów.

To jest kopiuj wklej
W przeglądarce jest wynik console.log(msg); czyli obiekt
testa:72 {POL: 'dane', part: '18', data: 'ago1IDAgb2JqCjw8L0xlbmd0aCAxMjM1Lw=='}

Nie mam czego zmieniać
Ja wysyłam
form := Format('{"POL": "dane","part":"%s","data":"%s" }', [IntToStr(j), ebajty]);
Tak pokazuje to przeglądarka i tyle.

Obecna forma nie dość, że chaotyczna, to w dodatku pełna bugów.

Możesz to rozwinąć?
Wiesz, takie mówienie niewiele wprowadza do dyskusji.

Owszem zapis można skrócić ale czy to pomoże w debugowaniu i szukaniu problemów?

0

Temat obszedłem.
Zrezygnowałem z przesyłania wiadomości z danymi binarnymi.

Może dobre to jest w przypadku danych tekstowych ale nie binarnych za dużo piep... się z tym.
dodatkowo niby można przesłać do 1MB danych ale z obserwacji nie działa to w przypadku danych binarnych.
I jeżeli jest więcej to po prostu nie działa pewnie a przy dużych pdf powiedzmy 10MB to jest ponad 400 000 komunikatów ...

Po prostu wystawiam chwilowy serwer www idHTTPServer i udostępniam w nim tylko jeden plik pdf.
po stronie Javasript pobieram taki pdf bezpośrednio do blob i wysyłam do swojego serwera.
Paradoksalnie działa to szybciej i pewniej niż poprzednie rozwiązanie.

Blob po stronie Javascript odebrany z idHTTPServer;

Blob {size: 7137826, type: 'application/pdf'} <- 34 strony

drugi
Blob {size: 77873601, type: 'application/pdf'} <- 290 stron
ten dałby ponad 3 000 000 pakietów.

działa naprawdę bardzo ładnie.

1
karol75 napisał(a):

Może dobre to jest w przypadku danych tekstowych ale nie binarnych za dużo piep... się z tym.
dodatkowo niby można przesłać do 1MB danych ale z obserwacji nie działa to w przypadku danych binarnych.

Przecież nie wysyłasz danych binarnych — każdy fragment pliku jest konwertowany do stringa, więc to nie ma znaczenia co w tym stringu się znajduje.

I jeżeli jest więcej to po prostu nie działa pewnie a przy dużych pdf powiedzmy 10MB to jest ponad 400 000 komunikatów ...

10MB dzielone na pakiety po ~1MB to dziesięć pakietów.

W teorii, dlatego że częścią wiadomości będzie szablon JSON (kilkadziesiąt bajtów), a surowe dane z pliku PDF, po konwersji, zajmować będą o około 33-37% więcej pamięci. Finalnie wyszłoby z 14 pakietów.

Paradoksalnie działa to szybciej i pewniej niż poprzednie rozwiązanie.

Nie ma się co dziwić, w końcu JSON-y i inne tekstowe formaty plików konfiguracyjnych wymagają mnóstwa czasu CPU, nie tylko do konwersji danych do i z stringów, ale i parsowania zawartości. Bloatware z tego wychodzi, nic więcej.

0
furious programming napisał(a):

Przecież nie wysyłasz danych binarnych — każdy fragment pliku jest konwertowany do stringa, więc to nie ma znaczenia co w tym stringu się znajduje.
poprzednie rozwiązanie.

Ja mam na myśli całą transmisję od pliku pdf do zobaczenia w przeglądarce i wtedy trzeba prszesłać dane binarne także.
Zobacz porównanie plików wyżej.

furious programming napisał(a):

10MB dzielone na pakiety po ~1MB to dziesięć pakietów.

U mnie jest po 25 bajtów inaczej są błędy transmisji po prostu pakiety nie dochodzą i dlatego tak liczę.

0
karol75 napisał(a):

U mnie jest po 25 bajtów inaczej są błędy transmisji po prostu pakiety nie dochodzą i dlatego tak liczę.

W takim razie używanie do tego JSON i Base64 jest totalną stratą czasu CPU i przepustowości łącza, dlatego bardzo dobrze, że znalazłeś inne (lepsze) rozwiązanie.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.