# CreativePDF — API v1 Editar um PDF que já existe, preservando tudo o que não foi editado. Não é uma API de gerar PDF. O motor foi escrito do zero: parser, codecs, motor de fontes, rasterizador, reconstrução de estrutura e writer. Esta API **recusa em vez de degradar em silêncio**. Um caractere que a fonte do documento não tem devolve 409 com a lista do que falta — e não um documento com a fonte trocada. Documentação para humanos: /creativepdf/api/ Contrato de máquina: /creativepdf/api/openapi.json (OpenAPI 3.1) A API é um binário auto-hospedado. Não existe endpoint público: `BASE` é onde você a roda (por padrão `http://localhost:8787`). ## AS CINCO REGRAS QUE EVITAM QUASE TODO ERRO 1. NUNCA construa uma âncora. Leia a estrutura da página e devolva o token como veio. Ele é opaco e assinado; interpretá-lo é o caminho mais rápido para escrever no lugar errado do arquivo. 2. RELEIA a estrutura depois de cada edição. Cada edição cria uma versão nova, e as âncoras da versão anterior não valem nela (409 stale_anchor). 3. MANDE `original` sempre, com o texto exato que a estrutura reportou. É conferido contra o arquivo antes de escrever. Sem ele, um índice deslocado escreveria no lugar errado em silêncio. 4. TRATE 409 font_missing_glyph como um PASSO, não como falha. Instale um doador em POST /v1/fonts, chame a rota de glifos, e repita a edição contra a versão nova. 5. LEIA `warnings`. `pushed` e `overflow` diferentes de zero significam que o texto novo não coube como estava: o arquivo saiu, mas o layout cedeu. ## AUTENTICAÇÃO Authorization: Bearer Cada inquilino vê só o que é dele. Documento de outro devolve 404 (não 403 — 403 confirmaria que o identificador adivinhado existe). Cotas: X-RateLimit-Limit e X-RateLimit-Remaining em toda resposta; 429 com Retry-After quando esgota. Janela deslizante de um minuto. Idempotência: cabeçalho `Idempotency-Key` em qualquer POST. Repetir devolve a mesma resposta com `Idempotency-Replayed: true`, guardada por 24h, e só quando deu certo. ## MODELO Documento é IMUTÁVEL. Editar cria uma VERSÃO. doc_7f3a… ver_1 o arquivo como entrou, byte a byte ver_2 ver_1 + as edições do pedido X ver_3 ver_2 + as edições do pedido Y `ver_1` sempre bate byte a byte com o que foi enviado. Baixar e comparar é como a garantia P1 se verifica de fora. ÂNCORA: o "onde" de uma edição. Token opaco, assinado por HMAC, preso ao par (documento, versão). Vem na estrutura da página, em cada linha e em cada palavra editável. A de palavra é o que permite trocar UMA CÉLULA de uma fileira de tabela — num PDF a fileira costuma sair de um operador só. Linha ou palavra SEM `anchor` não é editável: foi desenhada por mais de um operador, ou vem de um Form XObject usado em vários lugares do documento (trocar o texto mudaria todos os lugares). ## FLUXO CANÔNICO # 1. subir POST {BASE}/v1/documents Authorization: Bearer X-Filename: entrada.pdf → 201 { "id": "doc_…", "pages": 8, "versions": [{"id":"ver_1", …}] } # 2. ler a estrutura GET {BASE}/v1/documents/{doc}/versions/ver_1/pages/1 → 200 { "width":…, "height":…, "lines":[{ "text":…, "anchor":"…", "words":[{"text":…, "anchor":"…"}], … }], "fonts":[…], "tables":[…] } # 3. editar POST {BASE}/v1/documents/{doc}/versions/ver_1/edits Content-Type: application/json { "edits": [ { "anchor":"", "original":"", "text":"" } ] } → 201 { "version":"ver_2", "parent":"ver_1", "warnings": { "pushed":0.0, "overflow":0.0 } } # 4. conferir em pixels (opcional, recomendado) GET {BASE}/v1/documents/{doc}/versions/ver_2/pages/1/render?dpi=150 → 200 image/png # 5. baixar GET {BASE}/v1/documents/{doc}/versions/ver_2/file → 200 application/pdf Várias edições no mesmo pedido são aplicadas juntas. Se qualquer uma for recusada, NENHUMA é gravada. ## CAMPOS DE UMA EDIÇÃO anchor string OBRIGATÓRIO. Token da estrutura da página. original string OBRIGATÓRIO. O texto que está lá, para conferência. text string OBRIGATÓRIO. O texto novo. font_size number Corpo novo em pontos. Ausente/0 mantém o da linha. font string Id de uma fonte da sua biblioteca. Instala essa fonte no documento como recurso novo (só com os caracteres deste texto) e escreve o trecho com ela. É assim que negrito, itálico e troca de família chegam ao arquivo. Ausente mantém a fonte da linha. box_left number Borda esquerda da coluna em que o trecho vive. box_right number Borda direita. align integer 0 desconhecido · 1 esquerda · 2 direita · 3 centro · 4 justificado. A caixa (`box_left`, `box_right`, `align`) faz um valor que cresce numa coluna alinhada à direita crescer para dentro do vazio que o documento já deixava, em vez de empurrar a coluna vizinha. Os valores vêm da própria estrutura da página. ## QUANDO FALTA UM GLIFO (29% das edições em português) Um PDF embarca só os glifos que o documento já usava. Digitar um `ç` que a página não tinha esbarra num glifo ausente. A resposta: 409 { "error":"font_missing_glyph", "message":"a fonte deste trecho não tem estes caracteres", "missing":"ãçé%", "font":"F24", "page":3 } `missing` traz TODOS os caracteres que faltam, não o primeiro — para você subir o arquivo da fonte uma vez, não uma por caractere. Destravar: POST {BASE}/v1/fonts X-Font-Name: Calibri → 201 { "id":"font_…", "unitsPerEm":1000, "glyphs":2891, "outlines":"truetype" } POST {BASE}/v1/documents/{doc}/versions/{ver}/fonts/{resource}/glyphs { "donor":"font_…", "page":3, "characters":"ãçé%" } → 201 { "version":"ver_2", "added":5 } `{resource}` é o nome que a estrutura reporta em `fonts[].resource`: F1, R13, TT0. O doador precisa ter a MESMA `unitsPerEm` do subset embarcado — copiar contorno entre escalas perde precisão, e a API recusa em vez de aproximar. `added` pode ser maior que o número de caracteres: um `á` composto arrasta o `a` e o acento junto. A biblioteca de fontes é por inquilino e nunca compartilhada. ## ESTILO: FONTE QUE O DOCUMENTO NÃO TEM Negrito, itálico e troca de família são a mesma operação. Dois passos: POST {BASE}/v1/fonts → font_… (uma vez, por inquilino) POST .../edits com "font": "font_…" (por trecho) A fonte entra no documento como recurso novo, com só os caracteres do texto — não o arquivo inteiro. O resto da página não muda. Isto NÃO afrouxa o P4: sem `font`, um caractere que a fonte da linha não tem continua devolvendo 409 font_missing_glyph. Instalar é pedido explícito. Fonte que não está na sua biblioteca: 404 no_such_font. ## TRABALHO ADIADO Dispara com `Prefer: respond-async`, ou automaticamente quando o corpo passa de `async_above` (padrão 25 MiB). → 202 Location: /v1/jobs/job_… { "job":"job_…", "state":"queued" } GET {BASE}/v1/jobs/{job} → 200 { "job":…, "state":"succeeded", "status":201, "result":{ … } } `state`: queued | running | succeeded | failed. `result` é a resposta completa que a rota síncrona teria dado. Webhook opcional com `X-Webhook-Url`. O aviso chega assinado em `X-CreativePDF-Signature` (HMAC-SHA256 hex do corpo). O corpo inclui `finished_ms` — o instante entra dentro da assinatura, então um aviso capturado não vale para sempre. ## ROTAS GET /v1/health GET /v1/openapi.json POST /v1/documents cria (corpo = PDF) GET /v1/documents/{doc} metadados + versões DELETE /v1/documents/{doc} apaga tudo GET /v1/documents/{doc}/versions lista GET /v1/documents/{doc}/versions/{ver}/file baixa o PDF GET /v1/documents/{doc}/versions/{ver}/pages/{n} estrutura GET /v1/documents/{doc}/versions/{ver}/pages/{n}/render PNG (?dpi=12..600) POST /v1/documents/{doc}/versions/{ver}/edits edita POST /v1/documents/{doc}/versions/{ver}/fonts/{res}/glyphs acrescenta glifos GET /v1/fonts POST /v1/fonts instala (corpo = fonte) DELETE /v1/fonts/{font} GET /v1/jobs/{job} ## ERROS Forma: { "error": "", "message": "", … } 400 bad_json corpo não é JSON válido 400 bad_anchor âncora ilegível ou adulterada 400 bad_dpi dpi fora de 12–600 400 bad_edit align ou font_size fora da faixa 401 unauthorized chave ausente ou desconhecida 404 no_such_document não existe, ou é de outro inquilino 404 no_such_version idem 404 no_such_page idem 404 no_such_font doador não instalado 404 no_such_job idem 409 stale_anchor âncora de outra versão → releia a estrutura 409 font_missing_glyph + missing, font, page → instale doador 413 too_large passa de max_upload 422 not_a_pdf o arquivo não abre 422 not_a_font não reconhecido como fonte 422 password_required PDF cifrado 422 edit_refused a edição não pôde ser aplicada; message diz o quê 422 glyphs_refused doador recusado (escala de em, ou formato) 429 rate_limited cota do minuto; veja Retry-After Recusas comuns em `edit_refused` / `glyphs_refused`: - trecho vem de Form XObject usado em mais de um lugar - a palavra foi montada por mais de um operador - fonte composta com CMap que não é Identity - doador com outra escala de em - doador em CFF para subset TrueType (voltar de cúbica para quadrática é aproximação, e aproximar contorno é proibido pelo princípio P4) ## AS GARANTIAS P1 Nada intocado muda. Objeto não editado é reemitido byte a byte. P2 Imagem nunca é reencodada. O XObject original é copiado verbatim. P3 Reflow escopado. Editar um trecho acomoda o trecho, não recompõe a página. P4 Fonte certa ou nada. Glifo ausente é acrescentado à fonte, ou a API recusa. P5 O texto vetorial é a verdade. Content stream ganha do OCR, sempre. ## LIMITES (antes de expor) - Sem TLS. Ponha nginx ou Caddy na frente. - Conexões em paralelo (até 64), mas **uma edição por vez**: o que decide sobre um documento roda sob um mutex só. Para vazão, um processo por núcleo. - Job em andamento não sobrevive ao reinício; repita com a mesma Idempotency-Key. - A prévia (render) pode desenhar com fonte do acervo do servidor quando o PDF não embarca a sua. O arquivo nunca muda por isso: a edição continua recusando glifo ausente com 409. Para prévia igual entre máquinas, configure `system_fonts: false` e um `fonts_dir` versionado. - Webhook sai em texto claro; o HMAC garante que ninguém forjou, não que ninguém leu. - Sem `data_dir` configurado, nada sobrevive ao reinício — nem a chave que assina as âncoras, e aí toda âncora já emitida passa a dar 409. Privacidade: retenção padrão de 24h (configurável até zero), DELETE apaga de verdade, e o log não guarda conteúdo — identificador, tamanho, tempo e código de erro, nunca o texto editado.