Metadata en een link in elke pdf stempelen in SharePoint met Shareflex Rules en een Azure Function

In ons artikel over Shareflex Document Control en ISOPlanner legden we het idee uit: geldige documenten worden als pdf naar een ISOPlanner-bibliotheek gekopieerd, en elke pagina krijgt een koptekst “Shareflex Document Control” met de metadata van het document en een klikbare link terug naar de bron. Dit artikel is de technische kant: hoe een Shareflex Rules-event receiver en een kleine Azure Function metadata en een link in elke pdf in SharePoint stempelen.

PDF-linkstempel stap voor stap: Shareflex Rules-event, omzetten en plaatsen in ISOPlanner, Azure Function aanroepen, koptekst stempelen, bestand overschrijven.
PDF-linkstempel stap voor stap: Shareflex Rules-event, omzetten en plaatsen in ISOPlanner, Azure Function aanroepen, koptekst stempelen, bestand overschrijven.

Het resultaat: metadata en een link in de pdf-koptekst

Elke pagina van de pdf krijgt een compact kopblok: de kop “Shareflex Document Control”, de titel als klikbare link, en documenttype, gebied, periode en onderwerp. De oorspronkelijke inhoud van de pagina blijft precies zoals hij was.

Een gestempelde pdf in de ISOPlanner-bibliotheek: de koptekst Shareflex Document Control met klikbare titel en de documentmetadata (vroege testrun; documentinhoud vervaagd).
Een gestempelde pdf in de ISOPlanner-bibliotheek: de koptekst Shareflex Document Control met klikbare titel en de documentmetadata (vroege testrun; documentinhoud vervaagd).

De architectuur

  • Shareflex Rules stuurt het proces: een remote event receiver op de bibliotheek Geldige documenten reageert op ItemAdded – het moment dat een document wordt gepubliceerd.
  • SharePoint bewaart alles: de geldige documenten, de ISOPlanner-bibliotheek met mappen Gebied / Periode / Onderwerp, en een kleine lijst PDFStampSettings met de URL en sleutel van de function.
  • Een Azure Function (Node.js, pdf-lib) doet maar één ding: een pdf plus metadata ontvangen en de gestempelde pdf teruggeven.

De Azure Function is bewust “dom”: hij weet niets van SharePoint, ISOPlanner of Shareflex. Alle bedrijfslogica blijft in Shareflex Rules, waar ze samen met de rest van de oplossing in versiebeheer staat en wordt uitgerold – lees hoe we Shareflex ontwikkelen in VS Code met provisioning.

Stap 1 – De Shareflex-rule: het document als pdf plaatsen

Wordt een document geldig, dan leest de rule de metadata, zorgt dat de map ISOPlanner/Gebied/Periode/Onderwerp bestaat en plaatst het document daar als pdf. Office-bestanden worden omgezet met de ingebouwde Microsoft Graph-conversie van Shareflex; bestanden die al pdf zijn worden gekopieerd. (Veldnamen zijn hier vereenvoudigd.)

// Shareflex Rules - remote event receiver on the "Valid Documents" library (simplified)
import ShareflexRules from 'ShareflexRules';
import { config } from 'aoSite_Defaults_CUSTOM';
import { convertDocumentToPDF, isBuiltInConvertable } from 'ShareflexQualityDocuments_HelperFunctions';

const rules = new ShareflexRules(config);

performEvent().catch((e) => rules.logError('Copy to ISOPlanner failed', e));

async function performEvent() {
  if (rules.Event.type !== 'ItemAdded') return;           // only newly published documents

  const item = await rules.getEventItem('Id, Title, FileLeafRef, FileRef, qmDocumentType, sfArea, sfPeriod, sfTopic, qmRecordNo');
  const basePath = `${item.sfArea}/${item.sfPeriod}/${item.sfTopic}`;
  const isoPlannerListUrl = rules.Utils.combineUrls(rules.Context.siteUrl, '/ISOPlanner');

  // 1. Make sure ISOPlanner/Area/Period/Topic exists
  await rules.createFolder({ listUrl: isoPlannerListUrl, folderPath: basePath, ensureFolderPath: true, preCheck: true });

  // 2. Copy (PDF) or convert (Office) the valid document into that folder
  const fileName = await placeAsPdf(item, basePath, isoPlannerListUrl);
  if (!fileName) return;

  // 3. Stamp the Shareflex header + link into the placed PDF (fail-open)
  await stampLinkIntoPlacedPdf(item, basePath, fileName, isoPlannerListUrl);
}

Goed om te weten: de ingebouwde conversie kan niet direct in een submap uploaden. De pdf wordt daarom eerst in de root van de bibliotheek gemaakt en daarna met moveDocument naar de juiste map verplaatst.

Stap 2 – De pdf naar de Azure Function sturen

Staat de pdf op zijn plek, dan downloadt de rule hem, stuurt hem met de metadata in HTTP-headers naar de function en schrijft het gestempelde resultaat terug over hetzelfde bestand. Drie keuzes maken dit robuust:

  • Geen geheimen in code. De URL en sleutel van de function komen uit de lijst PDFStampSettings.
  • Een vaste link. De link wijst naar een Shareflex-redirectpagina met het recordnummer, zodat hij voor elke volgende revisie blijft werken.
  • Fail-open. Het plaatsen van de pdf is de eis; het stempelen is de extra. Is de function niet beschikbaar, dan wordt de fout gelogd en blijft de ongestempelde pdf staan.
async function stampLinkIntoPlacedPdf(item, basePath: string, fileName: string, isoPlannerListUrl: string) {
  try {
    // URL and function key live in a SharePoint settings list - never in code
    const settings = await getPdfStampSettings();             // reads Lists/PDFStampSettings
    if (!settings) return;

    const pdfBytes = await rules.getBinaryFileContentByUrl({ listUrl: isoPlannerListUrl, documentPath: `/${basePath}/${fileName}` });

    // Stable link: always opens the current record in Shareflex, whatever the revision
    const stableLink = `${rules.Context.siteUrl}/SitePages/Shareflex/View.aspx?$List=Redirect&targetList=Documents&qmRecordNo=${item.qmRecordNo}`;

    const connection = rules.ConnectionManager.createGenericHttpConnection({
      id: 'PdfLinkStamperConnection',
      baseUrl: settings.apiBaseUrl,
      headers: { 'x-functions-key': settings.apiKey },
    });

    const response: any = await rules.httpService({
      connectionId: connection.id,
      url: settings.apiBaseUrl,
      method: 'POST',
      headers: {
        'Content-Type': 'application/pdf',
        'X-Stamp-Link': stableLink,
        'X-Stamp-Title': encodeURIComponent(item.Title),
        'X-Stamp-Type': encodeURIComponent(item.qmDocumentType),
        'X-Stamp-Area': encodeURIComponent(item.sfArea),
        'X-Stamp-Period': encodeURIComponent(item.sfPeriod),
        'X-Stamp-Topic': encodeURIComponent(item.sfTopic),
      },
      body: pdfBytes,
      timeoutInSeconds: 60,
    });
    if (Number(response.status) >= 300) throw new Error(`Stamp failed: ${response.status}`);

    // The function returns Base64 text - write it back over the same file
    await rules.createFile({
      listUrl: isoPlannerListUrl,
      folderPath: basePath,
      fileName,
      content: String(response.body).trim(),
      encoding: 'Base64',
      overwrite: true,
      eventsActivated: false,                                  // don't trigger our own receiver again
    });
  } catch (error) {
    // Fail-open: the unstamped PDF stays in ISOPlanner, the error goes to the Shareflex log
    rules.logError(`Stamping failed for ISOPlanner/${basePath}/${fileName} - unstamped PDF left in place.`, error);
  }
}

Let op eventsActivated: false bij het terugschrijven: zonder die optie zou het overschrijven opnieuw events op de doelbibliotheek starten.

Stap 3 – De Azure Function: elke pagina stempelen met pdf-lib

De function is een HTTP-getriggerde Azure Function (v4-programmeermodel), beveiligd met een function key. Hij controleert de invoer, laadt de pdf met pdf-lib, tekent de koptekst op elke pagina en geeft het resultaat terug als Base64.

const { app } = require('@azure/functions');
const { PDFDocument, PDFName, PDFArray, StandardFonts, rgb } = require('pdf-lib');

app.http('StampLink', { methods: ['POST'], authLevel: 'function', route: 'StampLink', handler: stampLink });

async function stampLink(request, context) {
  const link = request.headers.get('x-stamp-link');
  let linkUrl;
  try {
    linkUrl = new URL(link);
    if (!['http:', 'https:'].includes(linkUrl.protocol)) throw new Error();
  } catch {
    return { status: 400, jsonBody: { error: "Missing or invalid 'X-Stamp-Link'." } };
  }

  const title  = decodeHeader(request.headers.get('x-stamp-title')) || link;
  const type   = decodeHeader(request.headers.get('x-stamp-type'));
  const area   = decodeHeader(request.headers.get('x-stamp-area'));
  const period = decodeHeader(request.headers.get('x-stamp-period'));
  const topic  = decodeHeader(request.headers.get('x-stamp-topic'));

  const pdfBytes = Buffer.from(await request.arrayBuffer());
  if (!pdfBytes.length || pdfBytes.length > 50 * 1024 * 1024) {
    return { status: 400, jsonBody: { error: 'Empty or too large PDF.' } };
  }

  let pdfDoc;
  try { pdfDoc = await PDFDocument.load(pdfBytes); }
  catch { return { status: 400, jsonBody: { error: 'Not a valid PDF.' } }; }

  const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
  const bold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);

  for (const page of pdfDoc.getPages()) {
    let y = page.getSize().height - 24;
    page.drawText('Shareflex Document Control', { x: 24, y, size: 12, font: bold, color: rgb(0.02, 0.14, 0.32) });
    y -= 14;
    drawLink(pdfDoc, page, font, context, 24, y, 'Title', title, linkUrl.toString());
    drawLabel(page, font, 284, y, 'Type', type);    y -= 14;
    drawLabel(page, font, 24, y, 'Area', area);
    drawLabel(page, font, 284, y, 'Period', period); y -= 14;
    drawLabel(page, font, 24, y, 'Topic', topic);
  }

  // Base64, because the Shareflex Rules HTTP client reads responses as a string
  return { status: 200, headers: { 'Content-Type': 'text/plain' },
           body: Buffer.from(await pdfDoc.save()).toString('base64') };
}

function decodeHeader(v) { if (!v) return ''; try { return decodeURIComponent(v); } catch { return v; } }

function drawLabel(page, font, x, y, label, value) {
  page.drawText(`${label}: ${value}`, { x, y, size: 9, font, color: rgb(0.1, 0.1, 0.1) });
}

function drawLink(pdfDoc, page, font, context, x, y, label, value, url) {
  const blue = rgb(0, 0.2, 0.6);
  page.drawText(`${label}: `, { x, y, size: 9, font, color: rgb(0.1, 0.1, 0.1) });
  const vx = x + font.widthOfTextAtSize(`${label}: `, 9);
  const w = font.widthOfTextAtSize(value, 9);
  page.drawText(value, { x: vx, y, size: 9, font, color: blue });
  page.drawLine({ start: { x: vx, y: y - 1.5 }, end: { x: vx + w, y: y - 1.5 }, thickness: 0.5, color: blue });

  // pdf-lib has no link API, so build the /Link annotation by hand (best effort)
  try {
    const ctx = pdfDoc.context;
    const annot = ctx.register(ctx.obj({
      Type: 'Annot', Subtype: 'Link', Border: [0, 0, 0],
      Rect: [vx, y - 2, vx + w, y + 11],
      A: { Type: 'Action', S: 'URI', URI: url },
    }));
    const annots = page.node.lookup(PDFName.of('Annots'));
    if (annots instanceof PDFArray) annots.push(annot);
    else page.node.set(PDFName.of('Annots'), ctx.obj([annot]));
  } catch (e) {
    context.warn('Link annotation failed - visible stamp still applied.', e);
  }
}

Drie lessen

  • Metadata in headers moet gecodeerd. HTTP-headers kunnen niet elk teken veilig vervoeren. Titels met spaties, accenten of speciale tekens gaan daarom met encodeURIComponent mee en worden in de function weer gedecodeerd.
  • Geef Base64 terug, geen bytes. De HTTP-client van Shareflex Rules behandelt het antwoord als tekst; binaire inhoud zou beschadigen. Base64-tekst gaat direct terug in createFile met encoding: 'Base64'.
  • pdf-lib heeft geen link-API. De klikbare link is een zelfgebouwde /Link-annotatie met een URI-actie. Dat is best effort: mislukt het bij een afwijkende pdf, dan staat de onderstreepte titel er nog steeds en maken de meeste viewers hem toch klikbaar.

Uitrollen

De function wordt vanuit VS Code uitgerold met de Azure Functions-extensie (of func azure functionapp publish). Daarna gaan de URL en de function key in de lijst PDFStampSettings in SharePoint – verder verandert er niets aan de Shareflex-kant. Dezelfde stempel is herbruikbaar voor andere Shareflex-oplossingen, bijvoorbeeld om een contractnummer en een link op elke pagina van een ondertekend contract te zetten.

Veelgestelde vragen

Hoe voeg je metadata toe aan de koptekst van een pdf vanuit SharePoint?

Lees de metadata van het SharePoint-item, stuur de pdf en de metadata naar een kleine service die op elke pagina een koptekst tekent (wij gebruiken een Azure Function met pdf-lib) en schrijf het resultaat terug naar de bibliotheek. In Shareflex wordt dit aangestuurd door een Shareflex Rules-event receiver.

Waarom een Azure Function en geen Power Automate?

Power Automate heeft geen standaardactie om tekst en link-annotaties in een bestaande pdf te tekenen zonder betaalde connector van derden. Een kleine Azure Function met pdf-lib doet het in milliseconden, is goedkoop en volledig in eigen beheer.

Waarom geeft de function Base64 terug?

De HTTP-client van Shareflex Rules leest antwoorden als tekst. Ruwe pdf-bytes raken daarbij beschadigd; Base64-tekst komt heel aan en kan direct met createFile worden teruggeschreven.

Wat gebeurt er als de Azure Function niet beschikbaar is?

De stap is fail-open: de pdf staat al in ISOPlanner voordat het stempelen begint. Mislukt het stempelen, dan wordt de fout gelogd in Shareflex en blijft de ongestempelde pdf staan.

Werkt de link in de pdf nog na een nieuwe revisie?

Ja. De link wijst naar een Shareflex-redirectpagina met het recordnummer van het document, niet naar een bestand of versie. Hij opent altijd het actuele record.

Dit ook in uw documentbeheersing?

De pdf-linkstempel werkt met Shareflex Document Control voor SharePoint en kan worden gekoppeld aan ISOPlanner of elke andere plek waar uw pdf’s terechtkomen. Neem contact op voor een demo.