Initial commit
This commit is contained in:
397
skills/presentation-master/adapters/pptx-adapter.js
Normal file
397
skills/presentation-master/adapters/pptx-adapter.js
Normal file
@@ -0,0 +1,397 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* PPTX Adapter
|
||||
*
|
||||
* Converts presentation specification to PPTX format specification.
|
||||
* Integrates with pptx skill for actual file generation.
|
||||
*
|
||||
* Usage:
|
||||
* bun pptx-adapter.js --presentation presentation.json --output pptx-spec.json
|
||||
*
|
||||
* Output: PPTX specification that pptx skill can execute
|
||||
*/
|
||||
|
||||
// PSD Brand Colors
|
||||
const PSD_COLORS = {
|
||||
primary_teal: '6CA18A',
|
||||
dark_blue: '25424C',
|
||||
cream: 'FFFAEC',
|
||||
warm_gray: 'EEEBE4',
|
||||
black: '000000',
|
||||
white: 'FFFFFF'
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate PPTX specification from presentation
|
||||
*/
|
||||
function generatePptxSpec(presentation, options = {}) {
|
||||
const brand = options.brand || null;
|
||||
const outputPath = options.output_path || null;
|
||||
|
||||
const spec = {
|
||||
title: presentation.title,
|
||||
author: options.author || 'Geoffrey',
|
||||
subject: presentation.description || presentation.title,
|
||||
layout: '16x9',
|
||||
output_path: outputPath,
|
||||
slides: []
|
||||
};
|
||||
|
||||
// Apply default theme if brand specified
|
||||
if (brand === 'psd') {
|
||||
spec.theme = {
|
||||
background: PSD_COLORS.warm_gray,
|
||||
primary_color: PSD_COLORS.primary_teal,
|
||||
secondary_color: PSD_COLORS.dark_blue,
|
||||
text_color: PSD_COLORS.black,
|
||||
accent_color: PSD_COLORS.cream
|
||||
};
|
||||
}
|
||||
|
||||
// Convert slides
|
||||
for (const slide of presentation.slides) {
|
||||
const slideSpec = convertSlide(slide, brand);
|
||||
spec.slides.push(slideSpec);
|
||||
}
|
||||
|
||||
return spec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert individual slide based on pattern
|
||||
*/
|
||||
function convertSlide(slide, brand) {
|
||||
switch (slide.pattern) {
|
||||
case 'title':
|
||||
return convertTitleSlide(slide, brand);
|
||||
|
||||
case 'big-idea':
|
||||
return convertBigIdeaSlide(slide, brand);
|
||||
|
||||
case 'visual-caption':
|
||||
return convertVisualCaptionSlide(slide, brand);
|
||||
|
||||
case 'data-viz':
|
||||
return convertDataVizSlide(slide, brand);
|
||||
|
||||
case 'process':
|
||||
case 'timeline':
|
||||
return convertProcessSlide(slide, brand);
|
||||
|
||||
case 'transition':
|
||||
return convertTransitionSlide(slide, brand);
|
||||
|
||||
default:
|
||||
return convertDefaultSlide(slide, brand);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Title Slide
|
||||
*/
|
||||
function convertTitleSlide(slide, brand) {
|
||||
const spec = {
|
||||
type: 'title',
|
||||
background: brand === 'psd' ? PSD_COLORS.dark_blue : 'FFFFFF',
|
||||
elements: []
|
||||
};
|
||||
|
||||
// Title
|
||||
spec.elements.push({
|
||||
type: 'text',
|
||||
text: slide.title,
|
||||
x: '10%',
|
||||
y: '35%',
|
||||
w: '80%',
|
||||
h: '30%',
|
||||
fontSize: 48,
|
||||
bold: true,
|
||||
color: brand === 'psd' ? PSD_COLORS.cream : '000000',
|
||||
align: 'center',
|
||||
valign: 'middle',
|
||||
fontFace: 'Arial'
|
||||
});
|
||||
|
||||
// Subtitle if present
|
||||
if (slide.text) {
|
||||
spec.elements.push({
|
||||
type: 'text',
|
||||
text: slide.text,
|
||||
x: '15%',
|
||||
y: '65%',
|
||||
w: '70%',
|
||||
h: '10%',
|
||||
fontSize: 24,
|
||||
color: brand === 'psd' ? PSD_COLORS.cream : '666666',
|
||||
align: 'center',
|
||||
fontFace: 'Arial'
|
||||
});
|
||||
}
|
||||
|
||||
return spec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Big Idea Slide
|
||||
*/
|
||||
function convertBigIdeaSlide(slide, brand) {
|
||||
// Calculate font size based on text length
|
||||
const textLength = slide.title.length;
|
||||
const fontSize = Math.min(120, Math.max(60, Math.floor(300 / textLength)));
|
||||
|
||||
const spec = {
|
||||
type: 'big-idea',
|
||||
background: 'FFFFFF',
|
||||
elements: [{
|
||||
type: 'text',
|
||||
text: slide.title,
|
||||
x: '10%',
|
||||
y: '25%',
|
||||
w: '80%',
|
||||
h: '50%',
|
||||
fontSize,
|
||||
bold: true,
|
||||
color: brand === 'psd' ? PSD_COLORS.primary_teal : '000000',
|
||||
align: 'center',
|
||||
valign: 'middle',
|
||||
fontFace: 'Arial'
|
||||
}]
|
||||
};
|
||||
|
||||
return spec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visual + Caption Slide
|
||||
*/
|
||||
function convertVisualCaptionSlide(slide, brand) {
|
||||
const spec = {
|
||||
type: 'visual-caption',
|
||||
background: brand === 'psd' ? PSD_COLORS.warm_gray : 'FFFFFF',
|
||||
elements: []
|
||||
};
|
||||
|
||||
// Image (70-80% of slide)
|
||||
if (slide.image) {
|
||||
spec.elements.push({
|
||||
type: 'image',
|
||||
path: slide.image.url || slide.image.path,
|
||||
x: '5%',
|
||||
y: '5%',
|
||||
w: '90%',
|
||||
h: '70%',
|
||||
sizing: {
|
||||
type: 'contain',
|
||||
w: '90%',
|
||||
h: '70%'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Caption (bottom)
|
||||
spec.elements.push({
|
||||
type: 'text',
|
||||
text: slide.text || slide.title,
|
||||
x: '5%',
|
||||
y: '80%',
|
||||
w: '90%',
|
||||
h: '15%',
|
||||
fontSize: 24,
|
||||
color: '000000',
|
||||
align: 'center',
|
||||
valign: 'top',
|
||||
fontFace: 'Arial'
|
||||
});
|
||||
|
||||
return spec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data Visualization Slide
|
||||
*/
|
||||
function convertDataVizSlide(slide, brand) {
|
||||
const spec = {
|
||||
type: 'data-viz',
|
||||
background: brand === 'psd' ? PSD_COLORS.warm_gray : 'FFFFFF',
|
||||
elements: []
|
||||
};
|
||||
|
||||
// Header bar if PSD brand
|
||||
if (brand === 'psd') {
|
||||
spec.elements.push({
|
||||
type: 'rect',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: '100%',
|
||||
h: '12%',
|
||||
fill: { color: PSD_COLORS.dark_blue }
|
||||
});
|
||||
|
||||
spec.elements.push({
|
||||
type: 'text',
|
||||
text: slide.title,
|
||||
x: '5%',
|
||||
y: '1%',
|
||||
w: '90%',
|
||||
h: '10%',
|
||||
fontSize: 36,
|
||||
bold: true,
|
||||
color: PSD_COLORS.cream,
|
||||
fontFace: 'Arial'
|
||||
});
|
||||
} else {
|
||||
spec.elements.push({
|
||||
type: 'text',
|
||||
text: slide.title,
|
||||
x: '5%',
|
||||
y: '5%',
|
||||
w: '90%',
|
||||
h: '10%',
|
||||
fontSize: 36,
|
||||
bold: true,
|
||||
color: '000000',
|
||||
fontFace: 'Arial'
|
||||
});
|
||||
}
|
||||
|
||||
// Chart/Data visualization
|
||||
if (slide.chart || slide.image) {
|
||||
const yOffset = brand === 'psd' ? '18%' : '18%';
|
||||
spec.elements.push({
|
||||
type: 'image',
|
||||
path: (slide.chart || slide.image).url || (slide.chart || slide.image).path,
|
||||
x: '7.5%',
|
||||
y: yOffset,
|
||||
w: '85%',
|
||||
h: '70%',
|
||||
sizing: {
|
||||
type: 'contain',
|
||||
w: '85%',
|
||||
h: '70%'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return spec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process/Timeline Slide
|
||||
*/
|
||||
function convertProcessSlide(slide, brand) {
|
||||
const spec = {
|
||||
type: 'process',
|
||||
background: brand === 'psd' ? PSD_COLORS.warm_gray : 'FFFFFF',
|
||||
elements: []
|
||||
};
|
||||
|
||||
// Title
|
||||
spec.elements.push({
|
||||
type: 'text',
|
||||
text: slide.title,
|
||||
x: '5%',
|
||||
y: '5%',
|
||||
w: '90%',
|
||||
h: '12%',
|
||||
fontSize: 36,
|
||||
bold: true,
|
||||
color: brand === 'psd' ? PSD_COLORS.dark_blue : '000000',
|
||||
fontFace: 'Arial'
|
||||
});
|
||||
|
||||
// Process diagram
|
||||
if (slide.image) {
|
||||
spec.elements.push({
|
||||
type: 'image',
|
||||
path: slide.image.url || slide.image.path,
|
||||
x: '5%',
|
||||
y: '25%',
|
||||
w: '90%',
|
||||
h: '65%',
|
||||
sizing: {
|
||||
type: 'contain',
|
||||
w: '90%',
|
||||
h: '65%'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return spec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transition Slide
|
||||
*/
|
||||
function convertTransitionSlide(slide, brand) {
|
||||
const spec = {
|
||||
type: 'transition',
|
||||
background: 'FFFFFF',
|
||||
elements: [{
|
||||
type: 'text',
|
||||
text: slide.title,
|
||||
x: '10%',
|
||||
y: '35%',
|
||||
w: '80%',
|
||||
h: '30%',
|
||||
fontSize: 42,
|
||||
bold: true,
|
||||
color: brand === 'psd' ? PSD_COLORS.primary_teal : '000000',
|
||||
align: 'center',
|
||||
valign: 'middle',
|
||||
fontFace: 'Arial'
|
||||
}]
|
||||
};
|
||||
|
||||
return spec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default Slide (fallback)
|
||||
*/
|
||||
function convertDefaultSlide(slide, brand) {
|
||||
return convertTitleSlide(slide, brand);
|
||||
}
|
||||
|
||||
// CLI Interface
|
||||
function main() {
|
||||
const args = {};
|
||||
|
||||
for (let i = 2; i < process.argv.length; i++) {
|
||||
if (process.argv[i].startsWith('--')) {
|
||||
const key = process.argv[i].replace(/^--/, '');
|
||||
const value = process.argv[i + 1];
|
||||
args[key] = value;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!args.presentation) {
|
||||
console.error('Usage: bun pptx-adapter.js --presentation presentation.json [--brand psd] [--output pptx-spec.json]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const fs = require('fs');
|
||||
const presentation = JSON.parse(fs.readFileSync(args.presentation, 'utf-8'));
|
||||
|
||||
const options = {
|
||||
brand: args.brand || null,
|
||||
output_path: args['output-path'] || null,
|
||||
author: args.author || 'Geoffrey'
|
||||
};
|
||||
|
||||
const pptxSpec = generatePptxSpec(presentation, options);
|
||||
|
||||
if (args.output) {
|
||||
fs.writeFileSync(args.output, JSON.stringify(pptxSpec, null, 2));
|
||||
console.log(`PPTX specification written to ${args.output}`);
|
||||
} else {
|
||||
console.log(JSON.stringify(pptxSpec, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
main();
|
||||
}
|
||||
|
||||
export { generatePptxSpec, convertSlide };
|
||||
794
skills/presentation-master/adapters/slides-adapter.js
Normal file
794
skills/presentation-master/adapters/slides-adapter.js
Normal file
@@ -0,0 +1,794 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Google Slides Adapter
|
||||
*
|
||||
* Converts presentation specification to Google Slides API requests.
|
||||
* Integrates with google-workspace skill for authentication and API access.
|
||||
*
|
||||
* Usage:
|
||||
* bun slides-adapter.js --presentation presentation.json --account psd --output slides-spec.json
|
||||
*
|
||||
* Output: Google Slides API request specification that google-workspace skill can execute
|
||||
*/
|
||||
|
||||
// PSD Brand Colors (from psd-brand-guidelines skill)
|
||||
const PSD_COLORS = {
|
||||
primary_teal: '#6CA18A',
|
||||
dark_blue: '#25424C',
|
||||
cream: '#FFFAEC',
|
||||
warm_gray: '#EEEBE4',
|
||||
black: '#000000',
|
||||
white: '#FFFFFF'
|
||||
};
|
||||
|
||||
// Slide dimensions (16:9 aspect ratio)
|
||||
const SLIDE_DIMENSIONS = {
|
||||
width: { magnitude: 10, unit: 'INCHES' },
|
||||
height: { magnitude: 5.625, unit: 'INCHES' }
|
||||
};
|
||||
|
||||
// Convert points to EMU (English Metric Units) for Google Slides API
|
||||
function pointsToEmu(points) {
|
||||
return Math.round(points * 12700);
|
||||
}
|
||||
|
||||
// Convert inches to EMU
|
||||
function inchesToEmu(inches) {
|
||||
return Math.round(inches * 914400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Google Slides API requests for creating a presentation
|
||||
*/
|
||||
function generateSlidesRequests(presentation, options = {}) {
|
||||
const account = options.account || 'psd';
|
||||
const applyBrand = options.brand !== false;
|
||||
|
||||
const requests = [];
|
||||
|
||||
// 1. Create presentation
|
||||
const createRequest = {
|
||||
operation: 'create_presentation',
|
||||
params: {
|
||||
title: presentation.title,
|
||||
locale: 'en-US'
|
||||
}
|
||||
};
|
||||
|
||||
// 2. Delete default slide
|
||||
const deleteDefaultSlide = {
|
||||
operation: 'delete_object',
|
||||
params: {
|
||||
objectId: '{{DEFAULT_SLIDE_ID}}' // Will be replaced after creation
|
||||
}
|
||||
};
|
||||
|
||||
// 3. Apply master theme if PSD brand
|
||||
let masterThemeRequests = [];
|
||||
if (applyBrand && account === 'psd') {
|
||||
masterThemeRequests = generatePsdTheme();
|
||||
}
|
||||
|
||||
// 4. Create slides
|
||||
const slideRequests = [];
|
||||
for (let i = 0; i < presentation.slides.length; i++) {
|
||||
const slide = presentation.slides[i];
|
||||
const slideId = `slide_${i}`;
|
||||
|
||||
// Create slide
|
||||
slideRequests.push({
|
||||
operation: 'create_slide',
|
||||
params: {
|
||||
objectId: slideId,
|
||||
insertionIndex: i,
|
||||
slideLayoutReference: { predefinedLayout: 'BLANK' }
|
||||
}
|
||||
});
|
||||
|
||||
// Add slide content based on pattern
|
||||
const contentRequests = generateSlideContent(slide, slideId, applyBrand ? account : null);
|
||||
slideRequests.push(...contentRequests);
|
||||
}
|
||||
|
||||
return {
|
||||
account,
|
||||
presentation_title: presentation.title,
|
||||
requests: [
|
||||
createRequest,
|
||||
...masterThemeRequests,
|
||||
...slideRequests
|
||||
],
|
||||
post_creation_steps: [
|
||||
'Delete default slide if present',
|
||||
'Return shareable link with edit permissions'
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate PSD brand theme
|
||||
*/
|
||||
function generatePsdTheme() {
|
||||
return [
|
||||
{
|
||||
operation: 'update_page_properties',
|
||||
params: {
|
||||
objectId: '{{MASTER_SLIDE_ID}}',
|
||||
pageProperties: {
|
||||
pageBackgroundFill: {
|
||||
solidFill: {
|
||||
color: { rgbColor: hexToRgb(PSD_COLORS.warm_gray) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate slide content based on pattern
|
||||
*/
|
||||
function generateSlideContent(slide, slideId, brand) {
|
||||
const requests = [];
|
||||
|
||||
switch (slide.pattern) {
|
||||
case 'title':
|
||||
requests.push(...generateTitleSlide(slide, slideId, brand));
|
||||
break;
|
||||
|
||||
case 'big-idea':
|
||||
requests.push(...generateBigIdeaSlide(slide, slideId, brand));
|
||||
break;
|
||||
|
||||
case 'visual-caption':
|
||||
requests.push(...generateVisualCaptionSlide(slide, slideId, brand));
|
||||
break;
|
||||
|
||||
case 'data-viz':
|
||||
requests.push(...generateDataVizSlide(slide, slideId, brand));
|
||||
break;
|
||||
|
||||
case 'process':
|
||||
case 'timeline':
|
||||
requests.push(...generateProcessSlide(slide, slideId, brand));
|
||||
break;
|
||||
|
||||
case 'transition':
|
||||
requests.push(...generateTransitionSlide(slide, slideId, brand));
|
||||
break;
|
||||
|
||||
default:
|
||||
requests.push(...generateDefaultSlide(slide, slideId, brand));
|
||||
}
|
||||
|
||||
return requests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Title Slide Pattern
|
||||
*/
|
||||
function generateTitleSlide(slide, slideId, brand) {
|
||||
const requests = [];
|
||||
|
||||
if (brand === 'psd') {
|
||||
// Background
|
||||
requests.push({
|
||||
operation: 'update_page_properties',
|
||||
params: {
|
||||
objectId: slideId,
|
||||
pageProperties: {
|
||||
pageBackgroundFill: {
|
||||
solidFill: {
|
||||
color: { rgbColor: hexToRgb(PSD_COLORS.dark_blue) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Title text
|
||||
const titleBoxId = `${slideId}_title`;
|
||||
requests.push({
|
||||
operation: 'create_shape',
|
||||
params: {
|
||||
objectId: titleBoxId,
|
||||
shapeType: 'TEXT_BOX',
|
||||
elementProperties: {
|
||||
pageObjectId: slideId,
|
||||
size: {
|
||||
width: { magnitude: 8, unit: 'INCHES' },
|
||||
height: { magnitude: 2, unit: 'INCHES' }
|
||||
},
|
||||
transform: {
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
translateX: inchesToEmu(1),
|
||||
translateY: inchesToEmu(1.8),
|
||||
unit: 'EMU'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Insert title text
|
||||
requests.push({
|
||||
operation: 'insert_text',
|
||||
params: {
|
||||
objectId: titleBoxId,
|
||||
text: slide.title,
|
||||
insertionIndex: 0
|
||||
}
|
||||
});
|
||||
|
||||
// Style title text
|
||||
requests.push({
|
||||
operation: 'update_text_style',
|
||||
params: {
|
||||
objectId: titleBoxId,
|
||||
textRange: { type: 'ALL' },
|
||||
style: {
|
||||
fontFamily: 'Arial',
|
||||
fontSize: { magnitude: 48, unit: 'PT' },
|
||||
bold: true,
|
||||
foregroundColor: {
|
||||
opaqueColor: {
|
||||
rgbColor: brand === 'psd'
|
||||
? hexToRgb(PSD_COLORS.cream)
|
||||
: hexToRgb('#FFFFFF')
|
||||
}
|
||||
}
|
||||
},
|
||||
fields: 'fontFamily,fontSize,bold,foregroundColor'
|
||||
}
|
||||
});
|
||||
|
||||
// Center align
|
||||
requests.push({
|
||||
operation: 'update_paragraph_style',
|
||||
params: {
|
||||
objectId: titleBoxId,
|
||||
textRange: { type: 'ALL' },
|
||||
style: {
|
||||
alignment: 'CENTER'
|
||||
},
|
||||
fields: 'alignment'
|
||||
}
|
||||
});
|
||||
|
||||
return requests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Big Idea Slide Pattern
|
||||
*/
|
||||
function generateBigIdeaSlide(slide, slideId, brand) {
|
||||
const requests = [];
|
||||
|
||||
// Massive text in center
|
||||
const textBoxId = `${slideId}_bigidea`;
|
||||
requests.push({
|
||||
operation: 'create_shape',
|
||||
params: {
|
||||
objectId: textBoxId,
|
||||
shapeType: 'TEXT_BOX',
|
||||
elementProperties: {
|
||||
pageObjectId: slideId,
|
||||
size: {
|
||||
width: { magnitude: 8, unit: 'INCHES' },
|
||||
height: { magnitude: 3, unit: 'INCHES' }
|
||||
},
|
||||
transform: {
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
translateX: inchesToEmu(1),
|
||||
translateY: inchesToEmu(1.3),
|
||||
unit: 'EMU'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
requests.push({
|
||||
operation: 'insert_text',
|
||||
params: {
|
||||
objectId: textBoxId,
|
||||
text: slide.title,
|
||||
insertionIndex: 0
|
||||
}
|
||||
});
|
||||
|
||||
// Giant font size (60-120pt)
|
||||
const fontSize = Math.min(120, Math.max(60, Math.floor(300 / slide.title.length)));
|
||||
|
||||
requests.push({
|
||||
operation: 'update_text_style',
|
||||
params: {
|
||||
objectId: textBoxId,
|
||||
textRange: { type: 'ALL' },
|
||||
style: {
|
||||
fontFamily: 'Arial',
|
||||
fontSize: { magnitude: fontSize, unit: 'PT' },
|
||||
bold: true,
|
||||
foregroundColor: {
|
||||
opaqueColor: {
|
||||
rgbColor: brand === 'psd'
|
||||
? hexToRgb(PSD_COLORS.primary_teal)
|
||||
: hexToRgb('#000000')
|
||||
}
|
||||
}
|
||||
},
|
||||
fields: 'fontFamily,fontSize,bold,foregroundColor'
|
||||
}
|
||||
});
|
||||
|
||||
requests.push({
|
||||
operation: 'update_paragraph_style',
|
||||
params: {
|
||||
objectId: textBoxId,
|
||||
textRange: { type: 'ALL' },
|
||||
style: {
|
||||
alignment: 'CENTER'
|
||||
},
|
||||
fields: 'alignment'
|
||||
}
|
||||
});
|
||||
|
||||
return requests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visual + Caption Slide Pattern
|
||||
*/
|
||||
function generateVisualCaptionSlide(slide, slideId, brand) {
|
||||
const requests = [];
|
||||
|
||||
// Image (70-80% of slide)
|
||||
if (slide.image) {
|
||||
const imageId = `${slideId}_image`;
|
||||
requests.push({
|
||||
operation: 'create_image',
|
||||
params: {
|
||||
objectId: imageId,
|
||||
url: slide.image.url,
|
||||
elementProperties: {
|
||||
pageObjectId: slideId,
|
||||
size: {
|
||||
width: { magnitude: 9, unit: 'INCHES' },
|
||||
height: { magnitude: 4, unit: 'INCHES' }
|
||||
},
|
||||
transform: {
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
translateX: inchesToEmu(0.5),
|
||||
translateY: inchesToEmu(0.3),
|
||||
unit: 'EMU'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Caption (1 line, bottom)
|
||||
const captionId = `${slideId}_caption`;
|
||||
requests.push({
|
||||
operation: 'create_shape',
|
||||
params: {
|
||||
objectId: captionId,
|
||||
shapeType: 'TEXT_BOX',
|
||||
elementProperties: {
|
||||
pageObjectId: slideId,
|
||||
size: {
|
||||
width: { magnitude: 9, unit: 'INCHES' },
|
||||
height: { magnitude: 0.8, unit: 'INCHES' }
|
||||
},
|
||||
transform: {
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
translateX: inchesToEmu(0.5),
|
||||
translateY: inchesToEmu(4.6),
|
||||
unit: 'EMU'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
requests.push({
|
||||
operation: 'insert_text',
|
||||
params: {
|
||||
objectId: captionId,
|
||||
text: slide.text || slide.title,
|
||||
insertionIndex: 0
|
||||
}
|
||||
});
|
||||
|
||||
requests.push({
|
||||
operation: 'update_text_style',
|
||||
params: {
|
||||
objectId: captionId,
|
||||
textRange: { type: 'ALL' },
|
||||
style: {
|
||||
fontFamily: 'Arial',
|
||||
fontSize: { magnitude: 24, unit: 'PT' },
|
||||
foregroundColor: {
|
||||
opaqueColor: {
|
||||
rgbColor: hexToRgb('#000000')
|
||||
}
|
||||
}
|
||||
},
|
||||
fields: 'fontFamily,fontSize,foregroundColor'
|
||||
}
|
||||
});
|
||||
|
||||
requests.push({
|
||||
operation: 'update_paragraph_style',
|
||||
params: {
|
||||
objectId: captionId,
|
||||
textRange: { type: 'ALL' },
|
||||
style: {
|
||||
alignment: 'CENTER'
|
||||
},
|
||||
fields: 'alignment'
|
||||
}
|
||||
});
|
||||
|
||||
return requests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data Visualization Slide Pattern
|
||||
*/
|
||||
function generateDataVizSlide(slide, slideId, brand) {
|
||||
const requests = [];
|
||||
|
||||
// Header
|
||||
if (brand === 'psd') {
|
||||
const headerId = `${slideId}_header`;
|
||||
requests.push({
|
||||
operation: 'create_shape',
|
||||
params: {
|
||||
objectId: headerId,
|
||||
shapeType: 'RECTANGLE',
|
||||
elementProperties: {
|
||||
pageObjectId: slideId,
|
||||
size: {
|
||||
width: { magnitude: 10, unit: 'INCHES' },
|
||||
height: { magnitude: 0.8, unit: 'INCHES' }
|
||||
},
|
||||
transform: {
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
translateX: 0,
|
||||
translateY: 0,
|
||||
unit: 'EMU'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
requests.push({
|
||||
operation: 'update_shape_properties',
|
||||
params: {
|
||||
objectId: headerId,
|
||||
shapeProperties: {
|
||||
shapeBackgroundFill: {
|
||||
solidFill: {
|
||||
color: { rgbColor: hexToRgb(PSD_COLORS.dark_blue) }
|
||||
}
|
||||
}
|
||||
},
|
||||
fields: 'shapeBackgroundFill'
|
||||
}
|
||||
});
|
||||
|
||||
// Header text
|
||||
const headerTextId = `${slideId}_header_text`;
|
||||
requests.push({
|
||||
operation: 'create_shape',
|
||||
params: {
|
||||
objectId: headerTextId,
|
||||
shapeType: 'TEXT_BOX',
|
||||
elementProperties: {
|
||||
pageObjectId: slideId,
|
||||
size: {
|
||||
width: { magnitude: 9, unit: 'INCHES' },
|
||||
height: { magnitude: 0.6, unit: 'INCHES' }
|
||||
},
|
||||
transform: {
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
translateX: inchesToEmu(0.5),
|
||||
translateY: inchesToEmu(0.1),
|
||||
unit: 'EMU'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
requests.push({
|
||||
operation: 'insert_text',
|
||||
params: {
|
||||
objectId: headerTextId,
|
||||
text: slide.title,
|
||||
insertionIndex: 0
|
||||
}
|
||||
});
|
||||
|
||||
requests.push({
|
||||
operation: 'update_text_style',
|
||||
params: {
|
||||
objectId: headerTextId,
|
||||
textRange: { type: 'ALL' },
|
||||
style: {
|
||||
fontFamily: 'Arial',
|
||||
fontSize: { magnitude: 36, unit: 'PT' },
|
||||
bold: true,
|
||||
foregroundColor: {
|
||||
opaqueColor: {
|
||||
rgbColor: hexToRgb(PSD_COLORS.cream)
|
||||
}
|
||||
}
|
||||
},
|
||||
fields: 'fontFamily,fontSize,bold,foregroundColor'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Chart/Image placeholder
|
||||
if (slide.chart || slide.image) {
|
||||
const chartId = `${slideId}_chart`;
|
||||
const yOffset = brand === 'psd' ? 1.0 : 0.5;
|
||||
|
||||
requests.push({
|
||||
operation: 'create_image',
|
||||
params: {
|
||||
objectId: chartId,
|
||||
url: (slide.chart || slide.image).url,
|
||||
elementProperties: {
|
||||
pageObjectId: slideId,
|
||||
size: {
|
||||
width: { magnitude: 8.5, unit: 'INCHES' },
|
||||
height: { magnitude: 4, unit: 'INCHES' }
|
||||
},
|
||||
transform: {
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
translateX: inchesToEmu(0.75),
|
||||
translateY: inchesToEmu(yOffset),
|
||||
unit: 'EMU'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return requests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process/Timeline Slide Pattern
|
||||
*/
|
||||
function generateProcessSlide(slide, slideId, brand) {
|
||||
const requests = [];
|
||||
|
||||
// Title
|
||||
const titleId = `${slideId}_title`;
|
||||
requests.push({
|
||||
operation: 'create_shape',
|
||||
params: {
|
||||
objectId: titleId,
|
||||
shapeType: 'TEXT_BOX',
|
||||
elementProperties: {
|
||||
pageObjectId: slideId,
|
||||
size: {
|
||||
width: { magnitude: 9, unit: 'INCHES' },
|
||||
height: { magnitude: 0.8, unit: 'INCHES' }
|
||||
},
|
||||
transform: {
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
translateX: inchesToEmu(0.5),
|
||||
translateY: inchesToEmu(0.3),
|
||||
unit: 'EMU'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
requests.push({
|
||||
operation: 'insert_text',
|
||||
params: {
|
||||
objectId: titleId,
|
||||
text: slide.title,
|
||||
insertionIndex: 0
|
||||
}
|
||||
});
|
||||
|
||||
requests.push({
|
||||
operation: 'update_text_style',
|
||||
params: {
|
||||
objectId: titleId,
|
||||
textRange: { type: 'ALL' },
|
||||
style: {
|
||||
fontFamily: 'Arial',
|
||||
fontSize: { magnitude: 36, unit: 'PT' },
|
||||
bold: true,
|
||||
foregroundColor: {
|
||||
opaqueColor: {
|
||||
rgbColor: brand === 'psd'
|
||||
? hexToRgb(PSD_COLORS.dark_blue)
|
||||
: hexToRgb('#000000')
|
||||
}
|
||||
}
|
||||
},
|
||||
fields: 'fontFamily,fontSize,bold,foregroundColor'
|
||||
}
|
||||
});
|
||||
|
||||
// Process diagram/image
|
||||
if (slide.image) {
|
||||
const imageId = `${slideId}_process`;
|
||||
requests.push({
|
||||
operation: 'create_image',
|
||||
params: {
|
||||
objectId: imageId,
|
||||
url: slide.image.url,
|
||||
elementProperties: {
|
||||
pageObjectId: slideId,
|
||||
size: {
|
||||
width: { magnitude: 9, unit: 'INCHES' },
|
||||
height: { magnitude: 3.5, unit: 'INCHES' }
|
||||
},
|
||||
transform: {
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
translateX: inchesToEmu(0.5),
|
||||
translateY: inchesToEmu(1.5),
|
||||
unit: 'EMU'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return requests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transition Slide Pattern
|
||||
*/
|
||||
function generateTransitionSlide(slide, slideId, brand) {
|
||||
const requests = [];
|
||||
|
||||
// Simple centered text
|
||||
const textId = `${slideId}_transition`;
|
||||
requests.push({
|
||||
operation: 'create_shape',
|
||||
params: {
|
||||
objectId: textId,
|
||||
shapeType: 'TEXT_BOX',
|
||||
elementProperties: {
|
||||
pageObjectId: slideId,
|
||||
size: {
|
||||
width: { magnitude: 8, unit: 'INCHES' },
|
||||
height: { magnitude: 2, unit: 'INCHES' }
|
||||
},
|
||||
transform: {
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
translateX: inchesToEmu(1),
|
||||
translateY: inchesToEmu(1.8),
|
||||
unit: 'EMU'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
requests.push({
|
||||
operation: 'insert_text',
|
||||
params: {
|
||||
objectId: textId,
|
||||
text: slide.title,
|
||||
insertionIndex: 0
|
||||
}
|
||||
});
|
||||
|
||||
requests.push({
|
||||
operation: 'update_text_style',
|
||||
params: {
|
||||
objectId: textId,
|
||||
textRange: { type: 'ALL' },
|
||||
style: {
|
||||
fontFamily: 'Arial',
|
||||
fontSize: { magnitude: 42, unit: 'PT' },
|
||||
bold: true,
|
||||
foregroundColor: {
|
||||
opaqueColor: {
|
||||
rgbColor: brand === 'psd'
|
||||
? hexToRgb(PSD_COLORS.primary_teal)
|
||||
: hexToRgb('#000000')
|
||||
}
|
||||
}
|
||||
},
|
||||
fields: 'fontFamily,fontSize,bold,foregroundColor'
|
||||
}
|
||||
});
|
||||
|
||||
requests.push({
|
||||
operation: 'update_paragraph_style',
|
||||
params: {
|
||||
objectId: textId,
|
||||
textRange: { type: 'ALL' },
|
||||
style: {
|
||||
alignment: 'CENTER'
|
||||
},
|
||||
fields: 'alignment'
|
||||
}
|
||||
});
|
||||
|
||||
return requests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default Slide Pattern (fallback)
|
||||
*/
|
||||
function generateDefaultSlide(slide, slideId, brand) {
|
||||
// Use title slide pattern as default
|
||||
return generateTitleSlide(slide, slideId, brand);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Convert hex color to RGB object
|
||||
*/
|
||||
function hexToRgb(hex) {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ? {
|
||||
red: parseInt(result[1], 16) / 255,
|
||||
green: parseInt(result[2], 16) / 255,
|
||||
blue: parseInt(result[3], 16) / 255
|
||||
} : { red: 0, green: 0, blue: 0 };
|
||||
}
|
||||
|
||||
// CLI Interface
|
||||
function main() {
|
||||
const args = {};
|
||||
|
||||
for (let i = 2; i < process.argv.length; i++) {
|
||||
if (process.argv[i].startsWith('--')) {
|
||||
const key = process.argv[i].replace(/^--/, '');
|
||||
const value = process.argv[i + 1];
|
||||
args[key] = value;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!args.presentation) {
|
||||
console.error('Usage: bun slides-adapter.js --presentation presentation.json --account psd [--output slides-spec.json]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const fs = require('fs');
|
||||
const presentation = JSON.parse(fs.readFileSync(args.presentation, 'utf-8'));
|
||||
|
||||
const options = {
|
||||
account: args.account || 'psd',
|
||||
brand: args.brand !== 'false'
|
||||
};
|
||||
|
||||
const slidesSpec = generateSlidesRequests(presentation, options);
|
||||
|
||||
if (args.output) {
|
||||
fs.writeFileSync(args.output, JSON.stringify(slidesSpec, null, 2));
|
||||
console.log(`Google Slides specification written to ${args.output}`);
|
||||
} else {
|
||||
console.log(JSON.stringify(slidesSpec, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
main();
|
||||
}
|
||||
|
||||
export { generateSlidesRequests, generateSlideContent, hexToRgb };
|
||||
Reference in New Issue
Block a user