# Concatenare le azioni

Come passare l’output di un’azione direttamente alla successiva, con un esempio completo di pipeline in tre fasi e i casi in cui concatenare non è lo strumento giusto.

Ogni azione riceve un PDF e ne restituisce uno, quindi l’output di un’azione è
un input valido per la successiva. Questo permette di costruire una pipeline in
più passaggi a partire da azioni a scopo unico: unire qualche file, apporre una
filigrana sul risultato e poi crittografarlo, tutto in un solo script, passando
i byte da una chiamata all’altra in memoria.

Poiché l’API è *stateless* (tra una richiesta e l’altra non viene memorizzato
nulla), non c’è alcun riferimento da riutilizzare né alcuna pulizia da fare.
Ogni fase si limita a passare il corpo della propria risposta alla fase
successiva nel campo `file`.

## Come funziona

Un’azione a output singolo risponde con `200 OK` e `Content-Type:
application/pdf`. Il corpo è un PDF completo. Per concatenare, si prende quel
corpo e lo si invia come parte `file` della richiesta successiva, invece di
leggere un file dal disco. Solo il primissimo input e l’ultimissimo output
devono toccare il filesystem; tutto ciò che sta in mezzo resta in memoria.

## Una pipeline in tre fasi

Questo esempio trasforma due file di origine in un unico documento crittografato
e filigranato.

<Steps>

<Step title="Unire i file di origine">
  `POST /v1/merge_documents` con `cover.pdf` e `report.pdf` restituisce un unico
  PDF combinato. Vedere [Unire documenti PDF](/docs/api/merge-pdf-documents) per
  le opzioni di input.
</Step>

<Step title="Apporre la filigrana sui byte uniti">
  Inviare quel PDF nel campo `file` a `POST /v1/add_text_watermark`. La risposta
  è lo stesso documento con una filigrana su ogni pagina.
</Step>

<Step title="Crittografare i byte filigranati">
  Inviare il PDF filigranato a `POST /v1/add_password` con una `password`. La
  risposta è il documento finale, protetto. Scriverlo su disco.
</Step>

</Steps>

<Info>
  L’unione accetta i PDF di input come parti `file` ripetute oppure, dove
  ripetere il nome di un campo è scomodo (PHP, Ruby), come campi numerati da
  `file_1` a `file_10`. Qui sotto sono mostrate entrambe le forme; usare `file`
  ripetuto quando i documenti sono più di dieci.
</Info>

<CodeGroup>

```bash title="cURL"
# Each stage reads the previous PDF from stdin via `-F 'file=@-'`.
curl -sS https://api.pdfblocks.com/v1/merge_documents \
  -H 'X-API-Key: your_api_key' \
  -F file=@cover.pdf \
  -F file=@report.pdf |
curl -sS https://api.pdfblocks.com/v1/add_text_watermark \
  -H 'X-API-Key: your_api_key' \
  -F 'file=@-;filename=merged.pdf;type=application/pdf' \
  -F line_1='CONFIDENTIAL' \
  -F line_2='ACME, Inc.' |
curl -sS https://api.pdfblocks.com/v1/add_password \
  -H 'X-API-Key: your_api_key' \
  -F 'file=@-;filename=watermarked.pdf;type=application/pdf' \
  -F password='pa$$word' \
  -o final.pdf
```

```python title="Python"
# pip install requests
import requests

BASE = 'https://api.pdfblocks.com'
HEADERS = {'X-API-Key': 'your_api_key'}

# 1. Merge cover.pdf and report.pdf into one document.
with open('cover.pdf', 'rb') as cover, open('report.pdf', 'rb') as report:
    merged = requests.post(
        f'{BASE}/v1/merge_documents',
        headers=HEADERS,
        files=[('file', cover), ('file', report)],
    )
merged.raise_for_status()

# 2. Watermark the merged bytes: no temp file.
watermarked = requests.post(
    f'{BASE}/v1/add_text_watermark',
    headers=HEADERS,
    files={'file': ('merged.pdf', merged.content, 'application/pdf')},
    data={'line_1': 'CONFIDENTIAL', 'line_2': 'ACME, Inc.'},
)
watermarked.raise_for_status()

# 3. Encrypt the watermarked bytes.
final = requests.post(
    f'{BASE}/v1/add_password',
    headers=HEADERS,
    files={'file': ('watermarked.pdf', watermarked.content, 'application/pdf')},
    data={'password': 'pa$$word'},
)
final.raise_for_status()

with open('final.pdf', 'wb') as output:
    output.write(final.content)
```

```javascript title="Node.js"
// Node.js 18+
import { readFile, writeFile } from 'node:fs/promises';

const BASE = 'https://api.pdfblocks.com';
const headers = { 'X-API-Key': 'your_api_key' };

// Post a multipart form and return the response PDF as bytes.
async function post(path, form) {
  const response = await fetch(BASE + path, { method: 'POST', headers, body: form });
  if (!response.ok) throw new Error(`${path} failed: ${response.status}`);
  return new Uint8Array(await response.arrayBuffer());
}

// 1. Merge cover.pdf and report.pdf.
const mergeForm = new FormData();
mergeForm.append('file', new Blob([await readFile('cover.pdf')]), 'cover.pdf');
mergeForm.append('file', new Blob([await readFile('report.pdf')]), 'report.pdf');
const merged = await post('/v1/merge_documents', mergeForm);

// 2. Watermark the merged bytes.
const watermarkForm = new FormData();
watermarkForm.append('file', new Blob([merged]), 'merged.pdf');
watermarkForm.append('line_1', 'CONFIDENTIAL');
watermarkForm.append('line_2', 'ACME, Inc.');
const watermarked = await post('/v1/add_text_watermark', watermarkForm);

// 3. Encrypt the watermarked bytes.
const passwordForm = new FormData();
passwordForm.append('file', new Blob([watermarked]), 'watermarked.pdf');
passwordForm.append('password', 'pa$$word');
const final = await post('/v1/add_password', passwordForm);

await writeFile('final.pdf', final);
```

```php title="PHP"
<?php
$base = 'https://api.pdfblocks.com';
$headers = ['X-API-Key: your_api_key'];

// Post a multipart form and return the response body, or throw on error.
function post(string $url, array $headers, array $fields): string {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => $fields,
    ]);
    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    if ($status !== 200) {
        throw new RuntimeException("Request to $url failed with status $status.");
    }
    return $body;
}

// 1. Merge cover.pdf and report.pdf (numbered fields for the array input).
$merged = post("$base/v1/merge_documents", $headers, [
    'file_1' => new CURLFile('cover.pdf', 'application/pdf'),
    'file_2' => new CURLFile('report.pdf', 'application/pdf'),
]);

// 2. Watermark the merged bytes in memory (CURLStringFile, PHP 8.1+).
$watermarked = post("$base/v1/add_text_watermark", $headers, [
    'file' => new CURLStringFile($merged, 'merged.pdf', 'application/pdf'),
    'line_1' => 'CONFIDENTIAL',
    'line_2' => 'ACME, Inc.',
]);

// 3. Encrypt the watermarked bytes.
$final = post("$base/v1/add_password", $headers, [
    'file' => new CURLStringFile($watermarked, 'watermarked.pdf', 'application/pdf'),
    'password' => 'pa$$word',
]);

file_put_contents('final.pdf', $final);
```

```ruby title="Ruby"
# gem install http
require 'http'
require 'stringio'

BASE = 'https://api.pdfblocks.com'
HEADERS = { 'X-API-Key' => 'your_api_key' }

def post(path, fields)
  response = HTTP.headers(HEADERS).post("#{BASE}#{path}", form: fields)
  raise "#{path} failed: #{response.status}" unless response.status.success?
  response.body.to_s
end

# 1. Merge cover.pdf and report.pdf (numbered fields for the array input).
merged = post('/v1/merge_documents',
  file_1: HTTP::FormData::File.new('cover.pdf'),
  file_2: HTTP::FormData::File.new('report.pdf'))

# 2. Watermark the merged bytes in memory.
watermarked = post('/v1/add_text_watermark',
  file: HTTP::FormData::File.new(StringIO.new(merged),
    filename: 'merged.pdf', content_type: 'application/pdf'),
  line_1: 'CONFIDENTIAL',
  line_2: 'ACME, Inc.')

# 3. Encrypt the watermarked bytes.
final = post('/v1/add_password',
  file: HTTP::FormData::File.new(StringIO.new(watermarked),
    filename: 'watermarked.pdf', content_type: 'application/pdf'),
  password: 'pa$$word')

File.write('final.pdf', final)
```

```go title="Go"
package main

import (
	"bytes"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
)

const base = "https://api.pdfblocks.com"

// post sends a multipart form and returns the response PDF bytes.
func post(path string, build func(*multipart.Writer)) []byte {
	var buf bytes.Buffer
	form := multipart.NewWriter(&buf)
	build(form)
	form.Close()

	req, _ := http.NewRequest("POST", base+path, &buf)
	req.Header.Set("Content-Type", form.FormDataContentType())
	req.Header.Set("X-API-Key", "your_api_key")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	body, _ := io.ReadAll(res.Body)
	if res.StatusCode != http.StatusOK {
		panic(fmt.Sprintf("%s failed: %d", path, res.StatusCode))
	}
	return body
}

func addFile(form *multipart.Writer, name string, data []byte) {
	part, _ := form.CreateFormFile("file", name)
	part.Write(data)
}

func main() {
	cover, _ := os.ReadFile("cover.pdf")
	report, _ := os.ReadFile("report.pdf")

	// 1. Merge cover.pdf and report.pdf.
	merged := post("/v1/merge_documents", func(form *multipart.Writer) {
		addFile(form, "cover.pdf", cover)
		addFile(form, "report.pdf", report)
	})

	// 2. Watermark the merged bytes.
	watermarked := post("/v1/add_text_watermark", func(form *multipart.Writer) {
		addFile(form, "merged.pdf", merged)
		form.WriteField("line_1", "CONFIDENTIAL")
		form.WriteField("line_2", "ACME, Inc.")
	})

	// 3. Encrypt the watermarked bytes.
	final := post("/v1/add_password", func(form *multipart.Writer) {
		addFile(form, "watermarked.pdf", watermarked)
		form.WriteField("password", "pa$$word")
	})

	os.WriteFile("final.pdf", final, 0644)
}
```

```csharp title="C#"
using System.Net.Http.Headers;

const string Base = "https://api.pdfblocks.com";

using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "your_api_key");

// Post a multipart form and return the response PDF bytes.
async Task<byte[]> Post(string path, Action<MultipartFormDataContent> build)
{
    using var form = new MultipartFormDataContent();
    build(form);
    var response = await client.PostAsync(Base + path, form);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadAsByteArrayAsync();
}

static void AddFile(MultipartFormDataContent form, string name, byte[] data)
{
    var content = new ByteArrayContent(data);
    content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
    form.Add(content, "file", name);
}

// 1. Merge cover.pdf and report.pdf.
var merged = await Post("/v1/merge_documents", form =>
{
    AddFile(form, "cover.pdf", File.ReadAllBytes("cover.pdf"));
    AddFile(form, "report.pdf", File.ReadAllBytes("report.pdf"));
});

// 2. Watermark the merged bytes.
var watermarked = await Post("/v1/add_text_watermark", form =>
{
    AddFile(form, "merged.pdf", merged);
    form.Add(new StringContent("CONFIDENTIAL"), "line_1");
    form.Add(new StringContent("ACME, Inc."), "line_2");
});

// 3. Encrypt the watermarked bytes.
var final = await Post("/v1/add_password", form =>
{
    AddFile(form, "watermarked.pdf", watermarked);
    form.Add(new StringContent("pa$$word"), "password");
});

await File.WriteAllBytesAsync("final.pdf", final);
```

</CodeGroup>

## Controllare ogni fase prima di passarne l’output alla successiva

Una fase non riuscita restituisce un `4XX` con un corpo
`application/problem+json`, non un PDF. Se quel corpo di errore viene immesso
nella richiesta successiva come `file`, anche la fase successiva fallisce, con
un messaggio poco chiaro. Controllare lo stato di ogni risposta prima di
passarne il corpo: gli esempi qui sopra sollevano un errore su qualsiasi
risposta diversa da `200`. Vedere [Errori](/docs/api/errors) per la forma della
risposta.

<Warning>
  Apporre la filigrana e unire prima di crittografare. Una volta che [Aggiungere
  una password](/docs/api/add-password-to-pdf) ha crittografato un documento, le
  azioni successive non possono più aprirlo, quindi qualsiasi passaggio con
  password va alla **fine** della pipeline. Vedere [Proteggere e sbloccare i
  documenti](/docs/api/protecting-documents) per tutte le regole sull’ordine.
</Warning>

## Quando concatenare e quando no

Concatenare quando ogni passaggio è una trasformazione distinta da applicare in
sequenza. Mantenere ogni passaggio una chiamata pura, a scopo unico: è questo
che rende l’output di uno un input valido per il successivo e che rende facile
individuare i guasti.

Alcuni lavori sembrano una concatenazione ma non lo sono. Riordinare e scartare
le pagine in un solo passaggio è un’unica chiamata a [Riordinare le
pagine](/docs/api/reorder-pages-of-pdf), non un’estrazione seguita da
un’unione. Ricorrere a una pipeline solo quando nessuna singola azione fa tutto
il lavoro.

## Vedi anche

<CardGroup cols={2}>

<Card title="Unire documenti PDF" href="/docs/api/merge-pdf-documents">
  Unire i file come prima fase di una pipeline.
</Card>

<Card title="Aggiungere una filigrana di testo" href="/docs/api/add-text-watermark-to-pdf">
  Apporre del testo sulle pagine di qualsiasi PDF.
</Card>

<Card title="Proteggere e sbloccare i documenti" href="/docs/api/protecting-documents">
  Mettere nell’ordine giusto le azioni di password e restrizione.
</Card>

<Card title="Lavorare con file di grandi dimensioni" href="/docs/api/working-with-large-files">
  Trasmettere in streaming input e output di una pipeline senza bufferizzarli.
</Card>

</CardGroup>
