Add Activepieces integration for workflow automation
- Add Activepieces fork with SmoothSchedule custom piece - Create integrations app with Activepieces service layer - Add embed token endpoint for iframe integration - Create Automations page with embedded workflow builder - Add sidebar visibility fix for embed mode - Add list inactive customers endpoint to Public API - Include SmoothSchedule triggers: event created/updated/cancelled - Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { promises as fs } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { nanoid } from 'nanoid';
|
||||
import Jimp from 'jimp';
|
||||
|
||||
const execPromise = promisify(exec);
|
||||
const pdftoppmPath = '/usr/bin/pdftoppm';
|
||||
|
||||
const MAX_FILE_SIZE = 16 * 1024 * 1024;
|
||||
|
||||
async function isPdftoppmInstalled(): Promise<boolean> {
|
||||
const { stdout, stderr } = await execPromise(`command -v ${pdftoppmPath}`);
|
||||
return !stderr && stdout.trim() === pdftoppmPath;
|
||||
}
|
||||
async function convertPdfToImages(dataBuffer: Buffer): Promise<Buffer[]> {
|
||||
const tempDir = tmpdir();
|
||||
const uniqueId = nanoid();
|
||||
const inputFilePath = join(tempDir, `input-${uniqueId}.pdf`);
|
||||
const outputDir = join(tempDir, `output-${uniqueId}`);
|
||||
try {
|
||||
await fs.mkdir(outputDir);
|
||||
await fs.writeFile(inputFilePath, dataBuffer as any);
|
||||
|
||||
const { stderr } = await execPromise(`${pdftoppmPath} -png ${inputFilePath} ${join(outputDir, 'output')}`);
|
||||
if (stderr) {
|
||||
throw new Error(stderr);
|
||||
}
|
||||
|
||||
const files = await fs.readdir(outputDir);
|
||||
const imageBuffers = [];
|
||||
for (const file of files) {
|
||||
const filePath = join(outputDir, file);
|
||||
const imageBuffer = await fs.readFile(filePath);
|
||||
await fs.unlink(filePath);
|
||||
imageBuffers.push(imageBuffer);
|
||||
}
|
||||
|
||||
return imageBuffers;
|
||||
} finally {
|
||||
await fs.unlink(inputFilePath).catch(() => void 0);
|
||||
await fs.rm(outputDir, { recursive: true, force: true }).catch(() => void 0);
|
||||
}
|
||||
}
|
||||
|
||||
async function concatImagesVertically(imageBuffers: Buffer[]): Promise<Buffer> {
|
||||
const images = await Promise.all(imageBuffers.map(buffer => Jimp.read(buffer)));
|
||||
const totalHeight = images.reduce((sum, image) => sum + image.getHeight(), 0);
|
||||
const maxWidth = Math.max(...images.map(image => image.getWidth()));
|
||||
|
||||
const finalImage = new Jimp(maxWidth, totalHeight);
|
||||
let yOffset = 0;
|
||||
|
||||
for (const image of images) {
|
||||
finalImage.composite(image, 0, yOffset);
|
||||
yOffset += image.getHeight();
|
||||
}
|
||||
|
||||
return finalImage.getBufferAsync(Jimp.MIME_PNG);
|
||||
}
|
||||
|
||||
export const convertToImage = createAction({
|
||||
name: 'convertToImage',
|
||||
displayName: 'Convert to Image',
|
||||
description: 'Convert a PDF file or URL to an image',
|
||||
props: {
|
||||
file: Property.File({
|
||||
displayName: 'PDF File or URL',
|
||||
required: true,
|
||||
}),
|
||||
imageOutputType: Property.StaticDropdown({
|
||||
displayName: 'Output Image Type',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Single Combined Image', value: 'single' },
|
||||
{ label: 'Separate Image for Each Page', value: 'multiple' },
|
||||
],
|
||||
},
|
||||
defaultValue: 'multiple',
|
||||
}),
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: {
|
||||
defaultValue: false,
|
||||
},
|
||||
retryOnFailure: {
|
||||
hide: true
|
||||
},
|
||||
},
|
||||
async run(context) {
|
||||
if (!await isPdftoppmInstalled()) {
|
||||
throw new Error(`${pdftoppmPath} is not installed`);
|
||||
}
|
||||
|
||||
const file = context.propsValue.file;
|
||||
const returnConcatenatedImage = context.propsValue.imageOutputType === 'single';
|
||||
// To prevent a DOS attack, we limit the file size to 16MB
|
||||
if (file.data.buffer.byteLength > MAX_FILE_SIZE) {
|
||||
throw new Error(`File size exceeds the limit of ${MAX_FILE_SIZE / (1024 * 1024)} MB.`);
|
||||
}
|
||||
|
||||
const dataBuffer = Buffer.from(file.data.buffer);
|
||||
|
||||
const imageBuffers = await convertPdfToImages(dataBuffer);
|
||||
|
||||
if (returnConcatenatedImage) {
|
||||
const finalImageBuffer = await concatImagesVertically(imageBuffers);
|
||||
const imageLink = await context.files.write({
|
||||
data: finalImageBuffer,
|
||||
fileName: `converted_image.png`,
|
||||
});
|
||||
|
||||
return {
|
||||
image: imageLink,
|
||||
};
|
||||
} else {
|
||||
const imageLinks = await Promise.all(imageBuffers.map((imageBuffer, index) =>
|
||||
context.files.write({
|
||||
data: imageBuffer,
|
||||
fileName: `converted_image_page_${index + 1}.png`,
|
||||
})
|
||||
));
|
||||
|
||||
return {
|
||||
images: imageLinks,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import { MarkdownVariant } from '@activepieces/shared';
|
||||
|
||||
export function pageRangeToIndexes(
|
||||
startPage: number,
|
||||
endPage: number,
|
||||
totalPages: number
|
||||
) {
|
||||
if (startPage > endPage) {
|
||||
throw Error(
|
||||
`Range start (${startPage}) has to be less than range end (${endPage})`
|
||||
);
|
||||
}
|
||||
|
||||
if (startPage === 0 || endPage === 0) {
|
||||
throw Error('Range start/end has to be a non-zero number');
|
||||
}
|
||||
|
||||
if (startPage > totalPages || endPage > totalPages) {
|
||||
throw Error(
|
||||
'Range start/end has to be less or equal to the total number of pages'
|
||||
);
|
||||
}
|
||||
|
||||
if (startPage < 0 && endPage > 0) {
|
||||
throw Error('Range start cannot be negative when end is positive');
|
||||
}
|
||||
|
||||
// page is 1 indexed, handle positive case
|
||||
let startIndex = startPage - 1;
|
||||
// handle negative case
|
||||
if (startPage < 0) {
|
||||
startIndex = totalPages + startPage;
|
||||
}
|
||||
|
||||
// page is 1 indexed, handle positive case
|
||||
let endIndex = endPage - 1;
|
||||
// handle negative case
|
||||
if (endPage < 0) {
|
||||
endIndex = totalPages + endPage;
|
||||
}
|
||||
|
||||
return Array.from(
|
||||
{ length: endIndex - startIndex + 1 },
|
||||
(_, idx) => startIndex + idx
|
||||
);
|
||||
}
|
||||
|
||||
const markdownValue = `
|
||||
This action can extract or rearrange the pages in a PDF.
|
||||
|
||||
- The order of array determines the sequence of pages.
|
||||
- Each array element is one inclusive continuous range with a start page and end page.
|
||||
- Pages start from 1, and 0 is not valid.
|
||||
- Start page has to be less than end page.
|
||||
- You can select one page by setting the same start and end page.
|
||||
- To select pages from the start, specify negative pages eg. -1 is the last page, -5 is the 5th last page. start: -5, end: -1 are the last 5 pages.
|
||||
- Range cannot span across 0 eg. start: -3, end: 5.
|
||||
`;
|
||||
|
||||
export const extractPdfPages = createAction({
|
||||
name: 'extractPdfPages',
|
||||
displayName: 'Extract PDF Pages',
|
||||
description: 'Extract or rearrange page(s)from PDF File.',
|
||||
props: {
|
||||
markdown: Property.MarkDown({
|
||||
variant: MarkdownVariant.INFO,
|
||||
value: markdownValue,
|
||||
}),
|
||||
file: Property.File({
|
||||
displayName: 'PDF File or URL',
|
||||
required: true,
|
||||
}),
|
||||
pageRanges: Property.Array({
|
||||
displayName: 'Page Ranges',
|
||||
properties: {
|
||||
startPage: Property.Number({
|
||||
displayName: 'Start Page',
|
||||
required: true,
|
||||
}),
|
||||
endPage: Property.Number({
|
||||
displayName: 'End Page',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: {
|
||||
defaultValue: false,
|
||||
},
|
||||
retryOnFailure: {
|
||||
hide: true,
|
||||
},
|
||||
},
|
||||
async run(context) {
|
||||
try {
|
||||
const srcDoc = await PDFDocument.load(context.propsValue.file.data as any);
|
||||
|
||||
const totalPages = srcDoc.getPageCount();
|
||||
const pageIndexes = context.propsValue.pageRanges.flatMap(
|
||||
(pageRange: any) =>
|
||||
pageRangeToIndexes(pageRange.startPage, pageRange.endPage, totalPages)
|
||||
);
|
||||
|
||||
const newDoc = await PDFDocument.create();
|
||||
const newPages = await newDoc.copyPages(srcDoc, pageIndexes);
|
||||
newPages.forEach((newPage) => newDoc.addPage(newPage));
|
||||
|
||||
const pdfBytes = await newDoc.save();
|
||||
const base64Pdf = Buffer.from(pdfBytes).toString('base64');
|
||||
|
||||
return context.files.write({
|
||||
data: Buffer.from(base64Pdf, 'base64'),
|
||||
fileName: context.propsValue.file.filename,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to extract pages: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import pdfParse from 'pdf-parse';
|
||||
|
||||
export const extractText = createAction({
|
||||
name: 'extractText',
|
||||
displayName: 'Extract Text',
|
||||
description: 'Extract text from PDF file or url',
|
||||
props: {
|
||||
file: Property.File({
|
||||
displayName: 'PDF File or URL',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: {
|
||||
defaultValue: false,
|
||||
},
|
||||
retryOnFailure: {
|
||||
hide: true
|
||||
},
|
||||
},
|
||||
async run(context) {
|
||||
const file = context.propsValue.file;
|
||||
const dataBuffer = Buffer.from(file.data.buffer);
|
||||
const pdfData = await pdfParse(dataBuffer);
|
||||
return pdfData.text;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,210 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { PDFDocument, PDFImage, RotationTypes, PageSizes } from 'pdf-lib';
|
||||
|
||||
export const imageToPdf = createAction({
|
||||
name: 'imageToPdf',
|
||||
displayName: 'Image to PDF',
|
||||
description: 'Convert image to PDF',
|
||||
props: {
|
||||
image: Property.File({
|
||||
displayName: 'image',
|
||||
description:
|
||||
'Image has to be png, jpeg or jpg and it will be scaled down to fit the page when image is larger than an A4 page',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: {
|
||||
defaultValue: false,
|
||||
},
|
||||
retryOnFailure: {
|
||||
hide: true,
|
||||
},
|
||||
},
|
||||
async run(context) {
|
||||
try {
|
||||
const image = context.propsValue.image;
|
||||
const imageExtension = image.extension?.toLowerCase();
|
||||
|
||||
const pdfDoc = await PDFDocument.create();
|
||||
const page = pdfDoc.addPage();
|
||||
const [pageWidth, pageHeight] = PageSizes.A4;
|
||||
page.setSize(pageWidth, pageHeight);
|
||||
|
||||
const xMargin = 30;
|
||||
const yMargin = 30;
|
||||
const [maxCardWidth, maxCardHeight] = [pageWidth - xMargin * 2, pageHeight - yMargin * 2];
|
||||
|
||||
let result: PDFImage | null = null;
|
||||
|
||||
if (imageExtension === 'png') {
|
||||
result = await pdfDoc.embedPng(image.data as any);
|
||||
} else if (imageExtension === 'jpg' || imageExtension === 'jpeg') {
|
||||
result = await pdfDoc.embedJpg(image.data as any);
|
||||
} else {
|
||||
throw new Error(`Unsupported image format: ${imageExtension}`);
|
||||
}
|
||||
|
||||
if (result === null) {
|
||||
throw new Error('Failed to embed image');
|
||||
}
|
||||
|
||||
const exifOrientation = getImageOrientation(image.data.buffer as any);
|
||||
const orientationCorrection = getOrientationCorrection(exifOrientation);
|
||||
|
||||
let scaledImage, correctedWidth, correctedHeight;
|
||||
switch (exifOrientation) {
|
||||
case ImageOrientation.FlipHorizontalRotate90:
|
||||
case ImageOrientation.Rotate90:
|
||||
case ImageOrientation.FlipVerticalRotate90:
|
||||
case ImageOrientation.Rotate270:
|
||||
// The uploaded image is rotated +/- 90 degrees
|
||||
scaledImage = result.scaleToFit(maxCardHeight, maxCardWidth);
|
||||
correctedWidth = scaledImage.height;
|
||||
correctedHeight = scaledImage.width;
|
||||
break;
|
||||
default:
|
||||
scaledImage = result.scaleToFit(maxCardWidth, maxCardHeight);
|
||||
correctedWidth = scaledImage.width;
|
||||
correctedHeight = scaledImage.height;
|
||||
}
|
||||
|
||||
let xShift, yShift;
|
||||
const yOffset = pageHeight - yMargin;
|
||||
switch (exifOrientation) {
|
||||
case ImageOrientation.FlipHorizontal:
|
||||
xShift = pageWidth - xMargin - correctedWidth;
|
||||
yShift = yOffset - correctedHeight;
|
||||
break;
|
||||
case ImageOrientation.Rotate180:
|
||||
xShift = xMargin + correctedWidth;
|
||||
yShift = yOffset;
|
||||
break;
|
||||
case ImageOrientation.FlipVertical:
|
||||
xShift = pageWidth - xMargin;
|
||||
yShift = yOffset;
|
||||
break;
|
||||
case ImageOrientation.FlipHorizontalRotate90:
|
||||
xShift = xMargin + correctedWidth;
|
||||
yShift = pageHeight - yOffset;
|
||||
break;
|
||||
case ImageOrientation.Rotate90:
|
||||
xShift = xMargin;
|
||||
yShift = yOffset;
|
||||
break;
|
||||
case ImageOrientation.FlipVerticalRotate90:
|
||||
xShift = xMargin;
|
||||
yShift = pageHeight - yOffset + correctedHeight;
|
||||
break;
|
||||
case ImageOrientation.Rotate270:
|
||||
xShift = xMargin + correctedWidth;
|
||||
yShift = yOffset - correctedHeight;
|
||||
break;
|
||||
default:
|
||||
xShift = xMargin;
|
||||
yShift = yOffset - correctedHeight;
|
||||
}
|
||||
|
||||
page.drawImage(result, {
|
||||
x: xShift,
|
||||
y: yShift,
|
||||
height: scaledImage.height,
|
||||
width: scaledImage.width,
|
||||
rotate: { angle: orientationCorrection.degrees, type: RotationTypes.Degrees },
|
||||
});
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
const base64Pdf = Buffer.from(pdfBytes).toString('base64');
|
||||
|
||||
return context.files.write({
|
||||
data: Buffer.from(base64Pdf, 'base64'),
|
||||
fileName: `${image.filename}.pdf`,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to convert text to PDF: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// https://sirv.com/help/articles/rotate-photos-to-be-upright/#exif-orientation-values
|
||||
enum ImageOrientation {
|
||||
Normal = 1, // "Image is in normal orientation, no rotation or flipping"
|
||||
Rotate90 = 6, // "Image is rotated 90 degrees"
|
||||
Rotate180 = 3, // "Image is rotated 180 degrees"
|
||||
Rotate270 = 8, // "Image is rotated 270 degrees"
|
||||
FlipHorizontal = 2, // "Image is flipped horizontally"
|
||||
FlipVertical = 4, // "Image is flipped horizontally and rotated 180 degrees"
|
||||
FlipHorizontalRotate90 = 5, // "Image is rotated 90 degrees and flipped horizontally"
|
||||
FlipVerticalRotate90 = 7, // "Image is rotated 270 degrees and flipped horizontally"
|
||||
Unknown= -1
|
||||
|
||||
}
|
||||
|
||||
// https://github.com/Hopding/pdf-lib/issues/1284
|
||||
// https://stackoverflow.com/questions/7584794/accessing-jpeg-exif-rotation-data-in-javascript-on-the-client-side/32490603#32490603
|
||||
function getImageOrientation(file: ArrayBuffer): ImageOrientation {
|
||||
const view = new DataView(file);
|
||||
|
||||
const length = view.byteLength;
|
||||
let offset = 2;
|
||||
|
||||
while (offset < length) {
|
||||
if (view.getUint16(offset + 2, false) <= 8) return ImageOrientation.Unknown;
|
||||
const marker = view.getUint16(offset, false);
|
||||
offset += 2;
|
||||
|
||||
// If EXIF buffer segment exists find the orientation
|
||||
if (marker == 0xffe1) {
|
||||
if (view.getUint32((offset += 2), false) != 0x45786966) {
|
||||
return ImageOrientation.Unknown;
|
||||
}
|
||||
|
||||
const little = view.getUint16((offset += 6), false) == 0x4949;
|
||||
offset += view.getUint32(offset + 4, little);
|
||||
const tags = view.getUint16(offset, little);
|
||||
offset += 2;
|
||||
for (let i = 0; i < tags; i++) {
|
||||
if (view.getUint16(offset + i * 12, little) == 0x0112) {
|
||||
const orientation = view.getUint16(offset + i * 12 + 8, little);
|
||||
switch (orientation) {
|
||||
case 1: return ImageOrientation.Normal;
|
||||
case 3: return ImageOrientation.Rotate180;
|
||||
case 6: return ImageOrientation.Rotate90;
|
||||
case 8: return ImageOrientation.Rotate270;
|
||||
case 2: return ImageOrientation.FlipHorizontal;
|
||||
case 4: return ImageOrientation.FlipVertical;
|
||||
case 5: return ImageOrientation.FlipHorizontalRotate90;
|
||||
case 7: return ImageOrientation.FlipVerticalRotate90;
|
||||
default: return ImageOrientation.Unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ((marker & 0xff00) != 0xff00) {
|
||||
break;
|
||||
} else {
|
||||
offset += view.getUint16(offset, false);
|
||||
}
|
||||
}
|
||||
return ImageOrientation.Unknown;
|
||||
}
|
||||
|
||||
function getOrientationCorrection(orientation: number): { degrees: number; mirrored?: 'x' | 'y' } {
|
||||
switch (orientation) {
|
||||
case ImageOrientation.FlipHorizontal:
|
||||
return { degrees: 0, mirrored: 'x' };
|
||||
case ImageOrientation.Rotate180:
|
||||
return { degrees: -180 };
|
||||
case ImageOrientation.FlipVertical:
|
||||
return { degrees: 180, mirrored: 'x' };
|
||||
case ImageOrientation.FlipHorizontalRotate90:
|
||||
return { degrees: 90, mirrored: 'y' };
|
||||
case ImageOrientation.Rotate90:
|
||||
return { degrees: -90 };
|
||||
case ImageOrientation.FlipVerticalRotate90:
|
||||
return { degrees: -90, mirrored: 'y' };
|
||||
case ImageOrientation.Rotate270:
|
||||
return { degrees: 90 };
|
||||
default:
|
||||
return { degrees: 0 };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
|
||||
export const mergePdfs = createAction({
|
||||
name: 'mergePdfs',
|
||||
displayName: 'Merge PDFs',
|
||||
description: 'Merges multiple PDF files into a single PDF document.',
|
||||
props: {
|
||||
pdfFiles: Property.Array({
|
||||
displayName: 'PDF Files',
|
||||
description: 'Array of PDF files to merge',
|
||||
required: true,
|
||||
properties: {
|
||||
file: Property.File({
|
||||
displayName: 'PDF File',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
outputFileName: Property.ShortText({
|
||||
displayName: 'Output File Name',
|
||||
description: 'Name for the merged PDF file (without extension)',
|
||||
required: false,
|
||||
defaultValue: 'merged-document',
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
try {
|
||||
const { pdfFiles, outputFileName } = context.propsValue;
|
||||
|
||||
// pdfFiles is already an array from Property.Array
|
||||
|
||||
if (!pdfFiles || !Array.isArray(pdfFiles) || pdfFiles.length < 2) {
|
||||
throw new Error('At least 2 PDF files are required for merging');
|
||||
}
|
||||
|
||||
const mergedPdf = await PDFDocument.create();
|
||||
|
||||
for (let i = 0; i < pdfFiles.length; i++) {
|
||||
const fileItem = pdfFiles[i] as any;
|
||||
const file = fileItem.file;
|
||||
|
||||
if (!file) {
|
||||
throw new Error(`File at index ${i} is null or undefined`);
|
||||
}
|
||||
|
||||
// Handle PDF files only
|
||||
let fileData: Buffer;
|
||||
const fileName = file.filename || file.name || `file-${i}.pdf`;
|
||||
|
||||
// Validate it's a PDF file
|
||||
if (fileName && !fileName.toLowerCase().endsWith('.pdf')) {
|
||||
throw new Error(`File at index ${i} (${fileName}) is not a PDF file`);
|
||||
}
|
||||
|
||||
if (file.data) {
|
||||
// Handle base64 strings
|
||||
if (typeof file.data === 'string') {
|
||||
fileData = Buffer.from(file.data, 'base64');
|
||||
}
|
||||
// Handle Buffer objects serialized as JSON
|
||||
else if (file.data.type === 'Buffer' && Array.isArray(file.data.data)) {
|
||||
fileData = Buffer.from(file.data.data);
|
||||
}
|
||||
// Handle direct Buffer
|
||||
else if (Buffer.isBuffer(file.data)) {
|
||||
fileData = file.data;
|
||||
}
|
||||
else {
|
||||
throw new Error(`Unsupported data format for PDF file ${i}: ${typeof file.data}`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new Error(`PDF file at index ${i} has no data property`);
|
||||
}
|
||||
|
||||
try {
|
||||
const pdfDoc = await PDFDocument.load(new Uint8Array(fileData));
|
||||
const pageCount = pdfDoc.getPageCount();
|
||||
const pageIndices = Array.from({ length: pageCount }, (_, idx) => idx);
|
||||
|
||||
const copiedPages = await mergedPdf.copyPages(pdfDoc, pageIndices);
|
||||
copiedPages.forEach((page) => mergedPdf.addPage(page));
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to process PDF file at index ${i} (${fileName}): ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const pdfBytes = await mergedPdf.save();
|
||||
|
||||
return context.files.write({
|
||||
data: Buffer.from(pdfBytes),
|
||||
fileName: `${outputFileName || 'merged-document'}.pdf`,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to merge PDFs: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
|
||||
export const pdfPageCount = createAction({
|
||||
name: 'pdfPageCount',
|
||||
displayName: 'PDF Page Count',
|
||||
description: 'Get page count of PDF file.',
|
||||
props: {
|
||||
file: Property.File({
|
||||
displayName: 'PDF File or URL',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: {
|
||||
defaultValue: false,
|
||||
},
|
||||
retryOnFailure: {
|
||||
hide: true,
|
||||
},
|
||||
},
|
||||
async run({ propsValue }) {
|
||||
try {
|
||||
const pdfDoc = await PDFDocument.load(propsValue.file.data as any);
|
||||
return pdfDoc.getPageCount();
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get page count: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { PDFDocument, StandardFonts } from 'pdf-lib';
|
||||
|
||||
export const textToPdf = createAction({
|
||||
name: 'textToPdf',
|
||||
displayName: 'Text to PDF',
|
||||
description: 'Convert text to PDF',
|
||||
props: {
|
||||
text: Property.LongText({
|
||||
displayName: 'text',
|
||||
description: 'Enter text to convert',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: {
|
||||
defaultValue: false,
|
||||
},
|
||||
retryOnFailure: {
|
||||
hide: true,
|
||||
},
|
||||
},
|
||||
async run(context) {
|
||||
const text = context.propsValue.text;
|
||||
|
||||
const pageSize: [number, number] = [595, 842]; // Standard A4 size
|
||||
const margin = 50;
|
||||
const topMargin = 70;
|
||||
const fontSize = 12;
|
||||
const lineSpacing = 5;
|
||||
const paragraphSpacing = 8;
|
||||
const fontType: StandardFonts = StandardFonts.Helvetica;
|
||||
|
||||
try {
|
||||
const pdfDoc = await PDFDocument.create();
|
||||
let page = pdfDoc.addPage(pageSize);
|
||||
const { width, height } = page.getSize();
|
||||
|
||||
const font = await pdfDoc.embedFont(fontType);
|
||||
|
||||
const lineHeight = font.heightAtSize(fontSize) + lineSpacing;
|
||||
const maxWidth = width - margin * 2;
|
||||
|
||||
const paragraphs = text.split('\n');
|
||||
let yPosition = height - topMargin;
|
||||
|
||||
paragraphs.forEach((paragraph) => {
|
||||
const words = paragraph.split(' ');
|
||||
let line = '';
|
||||
|
||||
words.forEach((word) => {
|
||||
const testLine = line + word + ' ';
|
||||
const testLineWidth = font.widthOfTextAtSize(testLine, fontSize);
|
||||
|
||||
if (testLineWidth > maxWidth) {
|
||||
page.drawText(line.trim(), {
|
||||
x: margin,
|
||||
y: yPosition,
|
||||
size: fontSize,
|
||||
font,
|
||||
});
|
||||
line = word + ' ';
|
||||
yPosition -= lineHeight;
|
||||
|
||||
if (yPosition < margin + lineHeight) {
|
||||
page = pdfDoc.addPage(pageSize);
|
||||
yPosition = height - topMargin;
|
||||
}
|
||||
} else {
|
||||
line = testLine;
|
||||
}
|
||||
});
|
||||
|
||||
if (line.trim()) {
|
||||
page.drawText(line.trim(), {
|
||||
x: margin,
|
||||
y: yPosition,
|
||||
size: fontSize,
|
||||
font,
|
||||
});
|
||||
yPosition -= lineHeight;
|
||||
}
|
||||
|
||||
yPosition -= paragraphSpacing;
|
||||
|
||||
if (yPosition < margin + lineHeight) {
|
||||
page = pdfDoc.addPage(pageSize);
|
||||
yPosition = height - topMargin;
|
||||
}
|
||||
});
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
const base64Pdf = Buffer.from(pdfBytes).toString('base64');
|
||||
|
||||
return context.files.write({
|
||||
data: Buffer.from(base64Pdf, 'base64'),
|
||||
fileName: 'text.pdf',
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to convert text to PDF: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user