Java (32) - tiskové služby

V minulém dílu (o základech tisku) se vyskytla zmínka o tiskových službách. Ty umožňují postoupit v úrovni abstrakce tisku ještě výše a nemuset zbytečně řešit technické detaily.

21.3.2007 09:00 | Lukáš Jelínek | přečteno 19933×

Abstrakce a virtualizace tisku

Za "normálních okolností" musí uživatel přemýšlet o tom, jaké vlastnosti má ta která tiskárna, má-li jich k dispozici více. Například potřebuje vědět, která tiskárna vytiskne stránku o velikosti A3, umí tisknout oboustranně nebo barevně. V praxi to znamená zjistit si předem potřebné informace a podle toho pak zvolit tiskárnu, na kterou se tisková úloha pošle.

Lepší je ovšem abstraktní pohled - máme nějaké dostupné tiskárny (místní či vzdálené, nebo i ryze virtuální, tvořené pouhým programem) a tiskovou úlohu s určitými parametry. Nyní jde o to, aby tisková úloha putovala na nejvhodnější tiskárnu a nemuselo se o tom příliš přemýšlet.

Tiskové služby v Javě

Standardní knihovny Javy obsahují kompletní aparát pro abstraktní pohled na tisk. Říká se mu tiskové služby a využívá se skrze aplikační rozhraní Java Print Service API. Podívejme se na to, co všechno umožňuje:

Není toho málo, implementace infrastruktury není jednoduchá, ale z hlediska použití v aplikacích je k dispozici velmi silná nástroj pro řízení tisku. Návrh JPS API vychází z protokolu IPP (Internet Printing Protocol).

Princip fungování tiskových služeb

Tisk s využitím tiskových služeb se poměrně značně liší od klasických metod tisku, třeba i té, kterou jsme používali minule. Základní postup při využití tiskových služeb je zhruba tento:

  1. příprava zdrojových dat (výchozího dokumentu) k tisku
  2. příprava datové třídy pro konkrétní formát tisku
  3. sestavení sady atributů pro popis požadavků na tisk
  4. vyhledání tiskárny (resp. tiskové služby), které vyhovuje požadavkům
  5. vytvoření tiskového dokumentu
  6. vytvoření tiskové úlohy
  7. odeslání úlohy tiskové službě

Rozdíl oproti klasickému postupu při tisku je jasně viditelný. Celý proces je sice poněkud náročnější a zdlouhavější, ale získáváme naopak větší flexibilitu a méně otrocké práce při tisku.

Příprava třídy pro datový formát

Zatímco při klasickém tisku je víceméně dáno, jak vypadá tištěný dokument (je to vždy grafický kontext, tedy virtuální plocha, na kterou se kreslí). Ne tak zde - možnosti tiskových služeb jsou mnohem větší.

Při specifikaci datového formátu se používá standard MIME. Jeho pomocí určujeme, jaký datový typ bude dokument mít. V základní podobě máme k dispozici tyto formáty:

Každý z popsaných formátů umožňuje používat různé zdroje dat (bajtová pole, streamy, URL apod.). Ne každý formát podporuje všechny zdroje. Než vytvoříme/získáme objekt pro práci s formátem, musíme tedy kromě formátu znát i zdroj dat.

Formát je reprezentován třídou DocFlavor z balíku javax.print. Instanci vytvoříme buď klasicky konstruktorem nebo využijeme některou z podtříd, připravených pro různé zdroje dat. Zde je několik příkladů získání instance DocFlavor:

DocFlavor df;

df = new DocFlavor("image/gif", "java.io.InputStream");
df = new DocFlavor("text/html; charset=utf-8", "java.io.Reader");
df = new DocFlavor.BYTE_ARRAY("application/octet-stream");
df = DocFlavor.URL.POSTSCRIPT;

První příkaz vytvoří konstruktorem instanci určenou pro obrázek GIF získávaný ze vstupního streamu. Druhý řádek je podobný, ukazuje ovšem HTML data včetně uvedení znakové sady (to je důležité, jinak se používá výchozí sada, což nemusí být vždy v pořádku) a data načítá z textově orientovaného streamu. Dále tu je instance vytvořená pomocí konstruktoru podtřídy (v tomto případě pro bajtové pole), určená pro automaticky detekovaný formát. A konečně poslední příklad plně využívá hotové objekty - v tomto případě se použije instance pro postscriptová data z URL.

Pokud známe předem (při implementaci programu) typ dat a jejich zdroj, používáme přednostně již vytvořené objekty nebo alespoň specializované třídy. Je to rychlejší než volat základní konstruktor s textovými parametry.

Atributy tisku

Požadavky na tiskovou službu, a také další vlastnosti tisku, se vyjadřují pomocí atributů. Každý atribut je instancí třídy implementující rozhraní javax.print.attribute.Attribute. Takových tříd je velké množství. Většinou nás bude zajímat například MediaSizeName (název velikosti média), PrinterResolution (rozlišení tiskárny), ColorSupported (podpora barev v tiskárně) nebo třeba PagesPerMinute (počet stránek za minutu). Většina atributů odpovídá příslušné reprezentaci v IPP.

S atributy je to ještě trochu složitější. Obecně se dají zařadit do pěti kategorií (podle účelu), každá z nich je představována jedním rozhraním. Atribut může patřit do více kategorií současně.

Všechny kategorie mají svůj smysl. Ještě se k nim dostaneme. Další důležitou věcí je sdružování atributů do sad. Základním rozhraním sady je AttributeSet, od něho jsou odvozena další rozhraní, například PrintRequestAttributeSet (sada požadavků na tiskovou službu).

Nejdříve se podíváme na využití atributů k vyhledání tiskové služby odpovídající daným požadavkům. Tady je krátký příklad, jak sestavit sadu atributů:

PrintRequestAttributeSet as = new HashPrintRequestAttributeSet(); 
as.add(MediaSize.ISO_A4);
as.add(Sides.DUPLEX);
as.add(new PrinterResolution(300, 300, PrinterResolution.DPI));

Jak je snad na první pohled zřejmé, takto sestavíme požadavek na tisk na stránku A4, s možností oboustranného (duplexního) tisku a s rozlišením 300 x 300 DPI. Lze používat jen atributy implementující rozhraní PrintRequestAttribute - z toho vyplývá, že některé hodnoty nelze přímo vyžadovat. Tam patří třeba rychlost tisku (PagesPerMinute; jedná se stejně jen o informativní hodnotu), ale také umístění tiskárny (PrinterLocation; sice by bylo určitě lákavé automaticky zabránit tisku na tiskárnu umístěnou ve firemní pobočce v Austrálii, nicméně umístění tiskárny je stejně jen textový atribut - rozhodnout musí uživatel).

Samostatnou zmínku si zaslouží speciální kategorie atributů - atributy podporovaných hodnot. Slouží k vyjádření hodnot podporovaných jinými atributy. Tak například atribut JobPrioritySupported říká, jaké hodnoty priorit úloh (JobPriority) tisková služba podporuje (ne všechny hodnoty z obecného rozsahu musí být vždy podporovány, případně nemusejí být podporovány žádné).

Vyhledání tiskové služby

Nyní nastal pravý čas zjistit, které tiskové služby vyhovují určeným požadavkům. Stačí jednoduše provést dotaz a pak už si jen vybírat z dostupných služeb:

DocFlavor fd = ...
PrintRequestAttributeSet as = ...

PrintService sa[] = PrintServiceLookup.lookupPrintServices(df, as);

Kdo má to štěstí, že mu takový dotaz vrátí větší počet služeb, může si pak vybrat nejvhodnější službu (viz dále). Mnohdy ale člověk může být rád, že vůbec nějaká služba prošla jeho sítem.

Celkem snadno se ale stane, že nevyhoví žádná služba - třeba když zadáme požadavek na duplexní tisk a žádná dostupná tiskárna ho nepodporuje. To je prostě smůla, takže nezbývá než si to někde v programu poznamenat a zkoušet dál s menšími požadavky. Lze to i opačně - začít s málem a postupně přidávat další a další požadavky, dokud nějaké služby zbývají.

Pokud si vystačíme s výchozí tiskárnou, stačí místo zmíněné metody zavolat lookupDefaultPrintService(). Ovšem pozor - nikde není řečeno, že nějaká tisková služba vůbec musí být k dispozici.

Ještě zbývá jedna důležitá věc, a to zjišťování atributů určité tiskové služby. To může sloužit například k ručnímu výběru služby uživatelem (například podle zmíněného umístění tiskáry, rychlosti tisku, aktuálního stavu tiskárny atd.). Jednoduše zavoláme instanci tiskové služby (PrintService) metodu getAttributes() a pak už pomocí metody get() přistupovat k atributům, nebo použít u služby přímo metodu getAttribute() se stejným výsledkem. Třída PrintService má ještě pár dalších zajímavých metod, které se mohou hodit při zkoumání, co služba umí.

Tiskový dokument

I když to není úplně nutné, velice se hodí vytvořit si něco, čemu se dá říkat "tiskový dokument". Jedná se vlastně o propojení zdroje tiskových dat, formátu tisku a tiskových atributů. Tiskový dokument je představován rozhraním Doc (z balíku javax.print). Ve většině případů nemusíme rozhraní implementovat, vyhoví totiž předem připravená třída SimpleDoc, obsahující vše potřebné.

Vytvoření instance SimpleDoc je triviální. Zavolá se konstruktor a tomu se předá zdroj dat, formát a atributy dokumentu. U těchto atributů stojí za to se chvilku zdržet. Říkají totiž, jaké parametry se mají při tisku nastavit. Služba by je pochopitelně měla podporovat - nemá smysl se snažit tisknout ze zásobníku, který tiskárna nemá.

Pokud atributy nepotřebujeme používat, stačí místo sady použít null - není třeba zbytečně vytvářet prázdnou sadu. Celý postup při vytvoření dokumentu může vypadat například takto:

HashDocAttributeSet as = new HashDocAttributeSet();
as.add(PrintQuality.DRAFT);
as.add(OrientationRequested.LANDSCAPE);
SimpleDoc sd = new SimpleDoc(data, df, as);

Atributy v příkladu určují použití kvalitativního režimu Draft a orientaci na šířku. Pozor na to, že některé atributy (například právě orientaci) lze použít jen u některých formátů - u jiných nemá smysl.

Tisková úloha a její odeslání

Zbývá už jen málo - vytvořit úlohu a poslat ji zvolené službě. Úloha jako takové se získá velmi snadno. Stačí instanci tiskové služby (PrintService) zavolat metodu createPrintJob(). Stejně jednoduše se pak úloha spustí - pouhým zavoláním metody print(). Jediné, na co se nesmí zapomenout, jsou požadované atributy služby (jako při jejím hledání). Nemusí se použít, pokud nejsou potřeba.

SimpleDoc sd = ...
PrintRequestAttributeSet as = ...
PrintService ps = ...
DocPrintJob job = ps.createPrintJob();

try {
  job.print(sd, as);
} catch (PrintException e) {
  ...
}

Všimněte si, že metoda print() může vyhodit výjimku, kterou musíme ošetřit. Navíc tato metoda obecně může (ale nemusí) pracovat asynchronně, proto její opuštění vůbec neznamená, že se něco vytisklo. Pro sledování stavu implementujeme rozhraní PrintJobListener a přidáme ho do úlohy (metodou addPrintJobListener()). Pak např. metoda printJobCompleted() znamená úspěšné dokončení úlohy, printJobFailed() selhání a tak podobně. Obdobně lze pomocí implementace rozhraní PrintJobAttributeListener sledovat změnu atributů úlohy (například změnu stavu).

Vlastní tisková služba

Pro některé účely se hodí vytvořit si vlastní tiskovou službu a pak ji používat. Může sloužit například k přímému exportu do některého grafického formátu.

Nejjednodušší možností je použít třídu StreamPrintServiceFactory. Ta umožňuje vyhledat službu odpovídající daným požadavkům. Vypadá to takto:

OutputStream os = ...

StreamPrintServiceFactory fa[] =
    StreamPrintServiceFactory.lookupStreamPrintServiceFactories(df,
        "application/pdf");

if (fa.length > 0) {
  StreamPrintService ps = fa[0].getPrintService(os);
  if (ps != null) {
    ...
  }
}

Příklad ukazuje vyhledání služby, která umožňuje (pro daný formát dokumentu) výstup do PDF. Je-li služba nalezena, připojí se na výstupní stream, který může představovat například otevřený soubor, síťové spojení, rouru do jiného vlákna apod.

Existuje ještě další možnost - naimplementovat si přímo celou tiskovou službu. Ta by se pak, pokud by měla být zahrnuta do vyhledávání podle požadavků, musela zaregistrovat zavoláním statické metody registerService() třídě PrintServiceLookup. Jen pro zajímavost, lze implementovat a registrovat také vlastní vyhledávače služeb (instance PrintServiceLookup; registrují se pomocí registerServiceProvider()), ale to už je poměrně okrajová záležitost.

Tiskové služby a grafický kontext

Zatím zůstávala opomenuta jedna otázka - jak propojit klasickou tiskovou metodu (z minulého dílu seriálu) s tiskovými službami. Jde to snadno a existují dokonce dvě rozdílné cesty, jak toho dosáhnout.

První z nich vychází z původního základu, avšak využije mechanismus tiskových služeb. Tady je příklad z minulého dílu upravený pro tiskové služby:

protected void doPrint(Printable pt) {
  PrinterJob pj = PrinterJob.getPrinterJob();
  pj.setPrintable(pt);
  PrintService sa[] = PrinterJob.lookupPrintServices();
  if (sa.length > 0) {
    try {
      pj.setPrintService(sa[0]); 
      pj.print();
    } catch (PrinterException e) {
      JOptionPane.showMessageDialog(this,
          "Při tisku došlo k chybě: " + e.getMessage(),
          "Chyba", JOptionPane.ERROR_MESSAGE);
    }
  }
}

Pokud bychom chtěli nastavovat atributy tisku, stačilo by vytvořit prázdnou sadu, pomocí známých dialogů (metody pageDialog() a printDialog(), zde ve verzích s předáním sady atributů) nastavit požadované hodnoty a pak sadu předat metodě print().

Druhou, v podstatě ještě jednodušší metodou je použít postup určený přímo pro tiskové služby a jako formát zvolit DocFlavor.SERVICE_FORMATTED.PRINTABLE. Při vytváření instance SimpleDoc se pak jako zdroj dat předá instance implementovaného rozhraní Printable. Podobně bychom si počínali i při použití rozhraní Pageable (použil by se formát DocFlavor.SERVICE_FORMATTED.PAGEABLE). Opět krátký příklad:

Printable pt = ...

PrintService sa[] =
    PrintServiceLookup.lookupPrintServices(
        DocFlavor.SERVICE_FORMATTED.PRINTABLE, null);
if (sa.length > 0) {
  SimpleDoc sd = new SimpleDoc(pt, DocFlavor.SERVICE_FORMATTED.PRINTABLE, null);
  ...
}

V příkladu se nepoužívají žádné atributy. Pokud bychom je chtěli použít, prázdné reference by se nahradily sadou atributů jako v příslušných příkladech z tohoto článku.

JavaBeans

Přístě konečně přijde řada na to, co kvůli dvěma článkům o tisku muselo počkat - tedy na úvod do JavaBeans. Věřím, že i tato oblast Javy bude neméně zajímavá, jako právě popsané záležitosti tisku.

Online verze článku: http://www.linuxsoft.cz/article.php?id_article=1433