Artykuł techniczny

Eksport zakresu komórek Excel jako jeden obraz w HotXLS

Czasem produktem nie jest dokument, tylko obraz tabeli. Blok podsumowania w mailu statusowym, wyrenderowany panel KPI w dashboardzie, miniatura obok wyniku wyszukiwania: wszystkie chcą komórek i żadne nie chcą papieru. TXLSCellImageExporter w HotXLS bierze klasyczny albo XLSX prostokąt komórek i produkuje jeden zwięzły PNG albo JPEG bez rozmiaru strony, bez marginesów, bez nagłówków czy stopek, bez tytułów wydruku i bez podziałów strony. Rozdzielczość, skala, format i jakość JPEG są konfigurowalne, obiekty, linie siatki i obramowania komórek mają niezależne przełączniki, tło może być kolorem albo przezroczyste, a zapis pliku przechodzi przez atomową zamianę w tym samym folderze, która zostawia istniejący cel nietknięty, gdy cokolwiek zawiedzie

Powód, dla którego to potrzebuje własnego eksportera, a nie flagi na ścieżce drukowania, jest taki, że paginacja nie jest opcjonalną warstwą, którą można wyłączyć. To jest rzecz, dla której potok stron istnieje

Czemu nie renderować zakresu przez potok drukowania?

Bo potok drukowania wstawia stronę między ciebie a komórki. Rozmiar papieru decyduje, ile się mieści, marginesy pchają treść do środka, nagłówki i stopki zajmują pasy, o które nikt nie prosił, tytuły wydruku powtarzają wiersze, które już masz, a podziały strony tną zakres. Blok podsumowania, który akurat ociera się o podział, wychodzi jako dwa obrazy z interesującym wierszem przeciętym na pół. Możesz to wszystko skompensować, ustawiając własny rozmiar strony dokładnie pasujący do zakresu, i ludzie to robią, ale to znaczy przeliczać geometrię papieru przy każdej zmianie zakresu i wciąż zostawia pas nagłówka i logikę tytułów wydruku w ścieżce

Eksporter komórek mierzy prostokąt, alokuje bitmapę dokładnie tego rozmiaru, rysuje do niej komórki i koduje. Nie ma strony, więc nie ma nic do skonfigurowania na około. Dla przypadków, gdy jednak chcesz papieru, właściwym narzędziem jest ścieżka eksportu PDF, omówiona w artykule o eksporcie arkusza do PDF

TXLSCellImageExporter mierzy, rysuje i koduje jeden obraz na zakres komórek, podczas gdy potok drukowania tnie zakres na podziałach stron
Potok stron wstawia geometrię papieru między ciebie a komórki; eksporter komórek nie ma strony nigdzie w ścieżce

Mierz, zanim wyrenderujesz

Measure zwraca wymiary w pikselach, które wyprodukowałyby aktualne ustawienia, bez kodowania czegokolwiek. To znaczy się z dwóch powodów. Szablon HTML albo mailowy zwykle potrzebuje wymiarów obrazu, zanim obraz istnieje, żeby zarezerwować ramkę i uniknąć przesunięcia układu. A usługa renderująca zakresy wybrane przez użytkowników potrzebuje sposobu odrzucenia absurdalnego żądania przed alokowaniem pod nie

uses
  lxHandleX, lxPagination;

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Exporter: TXLSCellImageExporter;
  Summary: TXLSXRange;
  W, H, Bytes: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarter.xlsx');
    Sheet := Book.Sheets.ByPos[0];
    Summary := Sheet.Range['A1:F20'];

    Exporter := TXLSCellImageExporter.Create;
    try
      Exporter.ImageFormat := xpifPng;   // PNG trzyma cienkie kreski ostre
      Exporter.DPI := 96;
      Exporter.Scale := 2.0;             // wyjście o gęstości retina
      Exporter.IncludeGridlines := False;
      Exporter.IncludeCellBorders := True;
      Exporter.TransparentBackground := True;
      Exporter.MaxPixels := 40 * 1000 * 1000;
      Exporter.MaxBytes := 8 * 1024 * 1024;

      if not Exporter.Measure(Summary, W, H) then
        raise Exception.Create('range exceeds the configured budget');
      // W i H są już znane; zarezerwuj ramkę układu przed kodowaniem
      Bytes := Exporter.Save(Summary, 'summary.png');
      if Bytes <= 0 then
        raise Exception.Create('image export failed, previous file kept');
    finally
      Exporter.Free;
    end;
  finally
    Book.Free;
  end;
end;

Budżety, bo skala się mnoży

MaxPixels i MaxBytes to nie defensorowa dekoracja. Liczba pikseli rośnie z kwadratem współczynnika skali i z kwadratem stosunku rozdzielczości, więc zakres będący rozsądnymi 1200 na 800 przy 96 DPI staje się w przybliżeniu 47 megapikselami przy 600 DPI, a użytkownik, który wybierze cały używany zakres zamiast bloku podsumowania, dokłada jeszcze rząd wielkości na wierzch. Bez sufitu trybem awarii jest alokacja, której proces nie jest w stanie spełnić, co zwala całą resztę pracy tego procesu

Z sufitem żądanie zawodzi, a wywołujący może wybrać: odmówić, zmniejszyć skalę albo zawęzić zakres. To znacznie lepsza pozycja dla serwera raportów i to samo rozumowanie stoi za jawnymi budżetami w dekoderze metafilów opisanym w artykule o ograniczonym dekoderze EMF i WMF

Przepływ budżetu dla TXLSCellImageExporter w HotXLS: Measure najpierw zwraca rozmiar w pikselach, potem MaxPixels i MaxBytes ograniczają alokację i rozmiar wyjścia
Odmowa dzieje się przed alokacją, a zawodzenie budżetu bajtów zostawia poprzedni obraz nietknięty dla wywołującego

Atomowa zamiana i czemu folder się liczy

Save do nazwy pliku nie pisze w cel. Pisze plik tymczasowy w tym samym folderze, koduje do niego i dopiero potem zamienia cel. Jeśli kodowanie zawiedzie, jeśli budżet zostanie przekroczony po drodze albo jeśli proces zostanie zabity, poprzedni obraz wciąż tam jest i wciąż jest ważny. Dashboard regenerujący swoje kafle według harmonogramu nigdy więc nie pokaże uciętego PNG, co jest typowym objawem naiwnego zapisu otwierającego cel i zaczynającego strumieniować

Szczegół tego samego folderu nie jest przypadkowy. Atomowa zamiana jest atomowa tylko w obrębie jednego woluminu, bo między woluminami system operacyjny musi skopiować, a potem usunąć, co przywraca okno, które chciano zamknąć. Każda implementacja tego wzorca, kładąca plik tymczasowy w systemowym katalogu tymczasowym, nie jest atomowa na maszynie, gdzie wyjście mieszka na innym dysku

Save w TXLSCellImageExporter koduje do pliku tymczasowego w tym samym folderze, potem atomowo zamienia cel; awarie zostawiają poprzedni obraz ważny
Plik tymczasowy musi mieszkać obok celu, bo atomowa zamiana działa tylko w obrębie jednego woluminu

Zdarzenia malowania rysują na prawdziwym canvasie

Eksporter zakresu i eksporter stron oba wystawiają wiodące i końcowe zdarzenia malowania i oba dostają pełny kontekst tylko do odczytu, a nie sam uchwyt canvasu. TXLSPagePaintContext niesie żywy canvas, granice w pikselach, rozmiar strony w punktach, rozdzielczość i skalę faktycznie w użyciu, numer strony dokumentu, numer strony w arkuszu, całkowitą liczbę stron, nazwę arkusza i źródłowy arkusz roboczy zarówno w odmianie klasycznej, jak i XLSX. To wystarczy, by narysować znak wodny skalujący się poprawnie albo pieczątkę strony znającą swoje miejsce w przebiegu

procedure TReportJob.StampDraft(Sender: TObject;
  const AContext: TXLSPagePaintContext);
begin
  // Świadome skali, więc pieczątka wygląda tak samo przy 1x i 3x
  AContext.Canvas.Font.Height := Round(-48 * AContext.Scale);
  AContext.Canvas.Font.Color := clSilver;
  AContext.Canvas.Brush.Style := bsClear;
  AContext.Canvas.TextOut(AContext.Bounds.Left + Round(24 * AContext.Scale),
    AContext.Bounds.Top + Round(24 * AContext.Scale), 'DRAFT');
end;

Exporter.AfterPaint := Job.StampDraft;

Trzy zachowania warte polegania. Zdarzenia odpalają dokładnie raz na wyrenderowaną klatkę, łącznie z każdą klatką wielostronicowego TIFF, więc licznik inkrementowany w handlerze jest godny zaufania. Pozostają ciche podczas pomiaru, więc handler ze skutkiem ubocznym nie biegnie dwa razy na jedno wyjście. A jeśli zdarzenie wiodące zgłosi wyjątek, zdarzenie końcowe nie odpala i żadne częściowe bajty obrazu nie są zapisywane, więc wyjątek w twoim własnym kodzie rysowania nie może wyprodukować pliku na pół opieczętowanego

Wybór formatu

PNG do wszystkiego bogatego w tekst. JPEG stosuje transformację blokową produkującą widoczne dzwonienie wokół cienkich, wysoko kontrastowych kresek, a dokładnie takie są obramowania komórek i mały tekst, i artefakty przeżywają przy ustawieniach jakości, przy których fotografia wygląda idealnie. JPEG zasługuje na swoje miejsce, gdy zakres dominują osadzone fotografie, a rozmiar pliku znaczy więcej niż wierność krawędzi. Przezroczyste tła wymagają PNG, bo JPEG nie ma kanału alfa, więc kafel przeznaczony na kolorowe podłoże podjął decyzję za ciebie

Jeśli twój zakres zawiera komórki scalone, skontroluj wyjście względem arkusza: scalone regiony wchodzą w interakcje z szerokościami kolumn w sposób, który ludzi zaskakuje, a reguły układu są omówione w artykule o scalonych komórkach i szablonach raportów. HotXLS czyta i pisze XLS, XLSX, ODS i CSV z Delphi i C++Builder bez zależności od Excela, a pełna powierzchnia eksportera jest udokumentowana na stronie produktu HotXLS Delphi spreadsheet component