Print Service Unificado — Documento de Contexto

Consolidado da conversa sobre automação de impressão no server1 (Windows Server). Cobre arquitetura, decisões, regras de domínio, código e passos de implementação.

Este service é a implementação técnica do que acontece na transição Pago → Ag. Impressão descrita em geracao-req-producao e conferencia-automatica-ag-impressao. Quando o webhook dispara, o print service coordena a impressão de fita, fichas e rótulos — os documentos que a produção consome.


1. Problema original

Na Neofarma, a impressão de rótulos na Epson ColorWorks C6000AU é manual: o operador abre o PDF no Adobe Reader, seleciona um preset de tamanho (85x25, 85x30, 85x40, 85x45mm), escolhe “rolo contínuo” como media source, confere no preview, e imprime. A largura é sempre 85mm; a altura varia conforme o produto. O rolo não tem marcas de detecção — o corte depende exclusivamente da altura informada ao driver.

O objetivo é eliminar essa intervenção manual, integrando a impressão de rótulos (e de fichas e fitas) num único fluxo automático disparado por evento do ERP.


2. Decisões de arquitetura

2.1 Evento único como gatilho

Quando o pedido muda de “pago” para “ag.impressão” na VPS, um webhook dispara POST /api/print-order no server1 via Cloudflare Tunnel. Nesse momento, todos os PDFs de rótulo já estão salvos na pasta do Google Drive. O service não precisa de polling, retry ou espera — os arquivos estarão lá.

2.2 Três impressões em sequência

JobImpressoraTecnologiaQuem gera o conteúdo
FitaBematech MP4200ESC/POS direto (socket/serial)Dados do pedido via ERP
FichasBrother A4PAD (UI automation) → PDF → SumatraPDFPAD abre ERP Delphi, gera PDF
RótulosEpson C6000AUGhostscript → driver Windows (mswinpr2)VPS gera PDFs, salva no Drive

A sequência é: fita → fichas → rótulos. Sequencial para evitar conflito no spooler.

2.3 Pasta unificada no Google Drive

G:\REQ\{numReq}\
  ├── ficha-{numReq}.pdf          ← gerado pelo PAD (server1)
  ├── {numReq}-a.pdf              ← rótulo produto A (gerado pela VPS)
  ├── {numReq}-b.pdf              ← rótulo produto B (gerado pela VPS)
  └── {numReq}-c.pdf              ← rótulo produto C (gerado pela VPS)

Convenção de nomes: fichas começam com ficha-, rótulos não. O service filtra por esse prefixo.

2.4 Ghostscript como renderizador (não .NET, não SumatraPDF para rótulos)

Ghostscript foi escolhido porque:

  • Controla DEVICEWIDTHPOINTS e DEVICEHEIGHTPOINTS por job via CLI
  • Usa o device mswinpr2 que imprime via driver Windows padrão
  • Não precisa de código .NET, PowerShell complexo, ou registrar Windows Forms dinâmicos
  • É CLI puro, chamável via child_process do Node

Limitação aceita: Ghostscript não controla media source (Roll Paper vs. Cut Sheet). A solução é configurar “Roll Paper” como default nas preferências do driver da Epson, uma vez só, no Painel de Controle do Windows.

2.5 PAD para fichas (não API, não GS direto)

O ERP Delphi não tem API de exportação de PDF. A única forma de extrair a ficha da requisição é via UI automation — o PAD simula: abrir ERP → navegar à REQ → clicar imprimir → selecionar “Microsoft Print to PDF” → salvar em G:\REQ\{numReq}\. Depois, o próprio PAD imprime na Brother via SumatraPDF CLI.

O Node chama o PAD via PAD.Console.Host.exe run --flow "Imprimir REQ" --input NumeroReq={numReq} e espera o processo terminar (timeout de 2 min).

Ver automacao-req-pad-erp para detalhamento completo do fluxo PAD.

2.6 SQLite para logs (não Firebird, não Postgres)

O SQLite local (C:\PrintService\print-log.db) registra cada job de impressão. Não usa o Firebird do ERP porque o log de impressão é operacional do server1, não dado de negócio. Consultável via API do próprio service.

2.7 Resposta assíncrona (202 Accepted)

O endpoint retorna 202 imediatamente para a VPS não ficar bloqueada. O processamento roda em background. A VPS pode consultar o status depois via GET /api/print-log/:orderId.


3. Regras de domínio — Rótulos e Epson C6000AU

3.1 Como o driver da Epson decide onde cortar

O C6000AU em rolo contínuo sem marcas depende do DEVMODE do print job para saber a altura do papel. Quando o Adobe Reader usa presets, ele está populando dmPaperLength no DEVMODE. O Ghostscript faz o equivalente via -dDEVICEHEIGHTPOINTS.

3.2 Dimensões e conversões

  • Largura fixa: 85mm
  • Altura: variável, extraída do MediaBox do PDF
  • Presets disponíveis: 25, 30, 40, 45mm (ordenados)
  • Conversão: 1pt = 1/72 polegada, 1 polegada = 25.4mm
  • ptToMm(pt) = (pt / 72) * 25.4
  • mmToPt(mm) = (mm / 25.4) * 72

3.3 Lógica de seleção de altura

// Snap para o menor preset que cabe o conteúdo
const preset = [25, 30, 40, 45].find(h => h >= contentHeightMm);
 
// Se conteúdo maior que todos os presets → usa altura exata arredondada
if (!preset) return Math.ceil(contentHeightMm);

Existe flag useExactHeight que, se true, ignora presets e usa a altura exata do PDF arredondada para cima ao próximo 0.5mm.

3.4 Extração da altura do PDF

const { PDFDocument } = require('pdf-lib');
const bytes = await readFile(pdfPath);
const doc = await PDFDocument.load(bytes);
const page = doc.getPage(0);
const { height } = page.getMediaBox(); // em pontos
const heightMm = ptToMm(height);

3.5 Flags do Ghostscript para rótulos

gswin64c.exe
  -dBATCH -dNOPAUSE -dNOSAFER
  -sDEVICE=mswinpr2
  -sOutputFile=%printer%Epson ColorWorks C6000AU
  -dDEVICEWIDTHPOINTS={largura em pt}
  -dDEVICEHEIGHTPOINTS={altura em pt}
  -dFitPage          ← escala conteúdo ao tamanho do papel
  -dNoCancel         ← sem diálogo de impressão
  arquivo.pdf

-dFitPage pode ser removido se os PDFs já são gerados com exatamente 85mm de largura.

3.6 Flags do Ghostscript para fichas A4

gswin64c.exe
  -dBATCH -dNOPAUSE -dNOSAFER
  -sDEVICE=mswinpr2
  -sOutputFile=%printer%Brother HL-XXXX series
  -sPAPERSIZE=a4
  -dFitPage -dNoCancel
  arquivo.pdf

(Na prática, a Brother é chamada pelo PAD via SumatraPDF, não pelo service diretamente.)


4. Regras de domínio — Fichas e PAD

4.1 Detecção de REQs novas

O PAD consulta o Firebird do ERP via ODBC:

SELECT NUMERO_REQ, DATA_INCLUSAO
FROM REQUISICAO
WHERE DATA_INCLUSAO > @ultima_verificacao
ORDER BY DATA_INCLUSAO

Compara contra a tabela SQLite processamentos para filtrar já processadas.

4.2 Controle SQLite do PAD

CREATE TABLE processamentos (
  id              INTEGER PRIMARY KEY AUTOINCREMENT,
  numero_req      TEXT NOT NULL,
  data_inclusao   TEXT,
  status          TEXT DEFAULT 'pendente',  -- pendente | ok | erro
  erro            TEXT,
  processado_em   TEXT DEFAULT (datetime('now','localtime'))
);

4.3 UI Automation — pontos de atenção

  • Waits de 2-3s entre ações (ERP Delphi tem latência variável)
  • Diálogo “Salvar como” do Print to PDF é janela do sistema — path preenchido dinamicamente
  • Verificação pós-salvamento: If file exists no PDF gerado
  • Timeout de erro: se o ERP travar, o PAD precisa abortar e registrar erro no SQLite

4.4 Impressão na Brother via SumatraPDF

& "C:\Tools\SumatraPDF\SumatraPDF.exe" -print-to "Brother HL-XXXX series" -silent "G:\REQ\{numReq}\ficha-{numReq}.pdf"

SumatraPDF é preferido sobre Start-Process -Verb PrintTo porque é silencioso e fecha sozinho.


5. Regras de domínio — Fita e MP4200

A MP4200 é controlada via ESC/POS direto (raw socket ou serial), sem passar pelo driver Windows. O código ESC/POS já existe e funciona — hoje é disparado por botão, passa a ser disparado pela função printFita(orderId) no service.

A fita não envolve PDF nem Ghostscript — é construção de buffer de bytes ESC/POS puro.


6. API do Print Service

6.1 Endpoints

POST /api/print-order
  Body: { "orderId": "PV-12345", "reqNumber": "123456" }
  Response: 202 { "message": "Pedido PV-12345 (REQ 123456) aceito para impressão" }
  Comportamento: processa em background, não bloqueia

GET /api/print-log/:orderId
  Response: { "orderId": "...", "jobs": [...] }

GET /api/print-log?limit=50
  Response: { "jobs": [...] }  (últimos N jobs, default 50, max 500)

6.2 Schema do SQLite de log

CREATE TABLE print_jobs (
  id              INTEGER PRIMARY KEY AUTOINCREMENT,
  order_id        TEXT NOT NULL,
  job_type        TEXT NOT NULL CHECK(job_type IN ('fita', 'fichas', 'rotulos')),
  file_name       TEXT,
  status          TEXT NOT NULL CHECK(status IN ('ok', 'error', 'skipped')),
  detail          TEXT,
  paper_height_mm REAL,
  created_at      TEXT DEFAULT (datetime('now', 'localtime'))
);

6.3 Exemplo de log

order_id | job_type | file_name        | status | detail                         | paper_height_mm
PV-123   | fita     | NULL             | ok     | NULL                           | NULL
PV-123   | fichas   | ficha-456789.pdf | ok     | NULL                           | NULL
PV-123   | rotulos  | 456789-a.pdf     | ok     | conteúdo: 22.3mm → papel: 25mm | 25
PV-123   | rotulos  | 456789-b.pdf     | ok     | conteúdo: 38.1mm → papel: 40mm | 40

7. Configuração do service

const CONFIG = {
  port: 3100,
  gsPath: 'gswin64c.exe',
  padExePath: 'C:\\Program Files\\Power Automate Desktop\\PAD.Console.Host.exe',
  padFlowName: 'Imprimir REQ',
  printers: {
    fita:    'Bematech MP-4200 TH',
    fichas:  'Brother HL-XXXX series',
    rotulos: 'Epson ColorWorks C6000AU',
  },
  rotulo: {
    widthMm: 85,
    presetHeightsMm: [25, 30, 40, 45],
  },
  driveBasePath: 'G:\\REQ',
  dbPath: 'C:\\PrintService\\print-log.db',
};

Todos os nomes de impressoras devem ser exatos conforme Get-Printer | Select-Object Name no PowerShell.


8. Pré-requisitos no Server1

SoftwareVersãoPropósito
Node.js18+Runtime do service
Ghostscript10.x (64-bit)Renderizar PDFs e enviar ao driver Windows
SumatraPDFPortableImpressão silenciosa de fichas na Brother (via PAD)
Power Automate DesktopUI automation no ERP Delphi
Firebird ODBC2.5PAD consulta banco do ERP
Google Drive for DesktopMonta G:\ com pastas de REQ
Cloudflare TunnelExpõe porta 3100 para a VPS

Dependências npm: express, pdf-lib, better-sqlite3

Configurações one-time no Windows

  1. Driver Epson C6000AU → Preferências → Paper Source: Roll Paper
  2. Ghostscript no PATH do sistema (C:\Program Files\gs\gs10.x\bin)
  3. DSN Firebird ODBC configurado para o banco do ERP
  4. CF Tunnel roteando para localhost:3100

9. Diagrama da arquitetura

                          VPS (nuvem)
                             │
              evento "pago → ag.impressão"
              + rótulos já salvos em G:\REQ\{req}\ via Drive
                             │
                      CF Tunnel (HTTPS)
                             │
                             ▼
                   ┌─────────────────────┐
                   │  Print Service      │
                   │  (Node + Express)   │
                   │  porta 3100         │
                   │  SQLite log local   │
                   └────────┬────────────┘
                            │
              ┌─────────────┼─────────────┐
              ▼             ▼             ▼
         1. FITA       2. FICHAS     3. RÓTULOS
         MP4200        Brother A4    Epson C6000AU
         ESC/POS       PAD→PDF→     GS → mswinpr2
         direto        SumatraPDF   (altura variável)
                            │
                    PAD abre ERP Delphi
                    via UI automation
                    salva PDF em G:\REQ\{req}\
                    imprime na Brother

        Todas as impressoras na rede local do server1

10. Fluxo causal detalhado

1. Atendente finaliza pedido na VPS
2. VPS muda status: "pago" → "ag.impressão"
3. VPS já gerou PDFs de rótulo e salvou em Google Drive (G:\REQ\{numReq}\)
4. VPS envia POST /api/print-order { orderId, reqNumber } via CF Tunnel
5. Server1 recebe, responde 202, inicia processamento background
6. [FITA] Service monta buffer ESC/POS com dados do pedido → envia à MP4200 → fita impressa
7. [FICHAS] Service chama PAD via CLI com reqNumber
   7a. PAD abre ERP Delphi
   7b. PAD navega à REQ, clica imprimir, salva PDF em G:\REQ\{numReq}\ficha-{numReq}.pdf
   7c. PAD chama SumatraPDF → imprime na Brother A4
   7d. Service verifica que o PDF existe no disco → log ok
8. [RÓTULOS] Service lista G:\REQ\{numReq}\*.pdf excluindo ficha-*
   8a. Para cada PDF de rótulo:
       - pdf-lib extrai MediaBox → altura em mm
       - Seleciona menor preset ≥ altura (25/30/40/45)
       - Ghostscript imprime via mswinpr2 na Epson com dimensão customizada
       - Driver corta o rolo na altura informada
       - Log com altura do conteúdo e altura do papel
9. Todos os jobs logados no SQLite
10. VPS pode consultar GET /api/print-log/{orderId} para verificar status

11. Decisões ainda abertas

TemaStatusNotas
Nome exato do fluxo PADA definirDepende de como for gravado no recorder
Caminho exato do PAD.Console.Host.exeA verificarVaria conforme versão instalada
-dFitPage nos rótulosA testarRemover se PDFs já são 85mm de largura
useExactHeight vs presetsComeçar com presetsTestar se preset produz margem indesejada; se sim, mudar para exato
Dados necessários para a fita ESC/POSIntegrar código existenteA função printFita está como placeholder
Tratamento de concorrênciaSequencial por enquantoSe volume crescer, avaliar fila (bull/bee-queue)

12. Referências cruzadas


13. Código-fonte

O arquivo print-service.js entregue junto com este documento contém o service completo com:

  • Express server na porta 3100
  • Endpoint POST /api/print-order (async, 202)
  • Endpoints de consulta GET /api/print-log
  • Job de fita (placeholder para ESC/POS existente)
  • Job de fichas (dispara PAD via CLI)
  • Job de rótulos (pdf-lib + Ghostscript com altura variável)
  • SQLite de log automático
  • CONFIG centralizado para ajustar nomes de impressoras, caminhos e presets