@cliftonc/pptx-to-json (英语)
一个用于将PowerPoint文件(PPTX)和PowerPoint剪贴板数据解析成结构化的JSON的TypeScript库. 提取文本、形状、图像和表格等组件及其定位、造型和内容信息.
现场直播
试试看 ppt-paste.clifton-cunningham. workers.dev. 互联网档案馆的存檔,存档日期2014-12-22
特征
- PPTX 文件分析: 分析完整的PowerPoint文件
- 剪贴板数据解析: 处理PowerPoint剪贴板粘贴操作
- 构成部分: 提取文本、形状、图像、表格和更多
- 位置样式( S):获得准确的定位、维度和造型信息
- 幻灯片结构: 发言顺序、标题和发言说明,通过 关系图而不是从文件名中猜出(参见 幻灯片结构(中文(简体) )
- 类型脚本支持: 包含全部类型脚本定义
- 模块结构: 不同成分类型的可扩展解析系统
安装
npm install @cliftonc/pptx-to-json
使用量
分析 PPTX 文件
import { PPTXParser, PowerPointParser } from '@cliftonc/pptx-to-json';
import fs from 'node:fs';
const buffer = fs.readFileSync('presentation.pptx');
// 1. unzip + parse every part to JSON
const json = await new PPTXParser().buffer2json(buffer);
// 2. turn that into slides and components
const result = await new PowerPointParser().parseJson(json);
for (const slide of result.slides) {
console.log(slide.slideNumber, slide.title); // presentation order, title placeholder
console.log(slide.notes); // speaker notes, if any
console.log(slide.components.length);
}
分析剪贴板数据
import { PowerPointClipboardProcessor } from '@cliftonc/pptx-to-json';
const processor = new PowerPointClipboardProcessor();
const result = await processor.parseClipboardBuffer(clipboardBuffer);
console.log(result.slides);
幻灯片结构
3个答案通过包关系图解决,因为很明显 快捷键在之后难以注意到的方式上是错误的 :
| 从 | 为什么不是明显的方式 | |
|---|---|---|
slide.slideNumber | ppt/presentation.xml → p:sldIdLst → r:id | 幻灯片 文件名是创建顺序当甲板重排时, PowerPoint 保留了幻灯片的部分名称, 所以 slide3.xml 可以是显示的第七张幻灯片. result.slideOrderSource 报告是否可以阅读这一清单; slide.metadata.fileSlideNumber 保留文件名编号. |
slide.notes | ppt/slides/_rels/slideN.xml.rels → notesSlide | 注释部分只针对有注释的幻灯片,所以 notesSlideM 停止排队 slideN 一旦一个幻灯片缺少它们。 由错误的幻灯片产生的音符比完全没有音符更糟糕. 只有身体占位符被读取: 几乎每个真实的音符部分只包含自动生成的幻灯片编号字段,否则会以"音符"的形式返回. |
slide.title | p:ph type="title"|"ctrTitle"的占位符 idx | 幻灯片的占位符经常省略 @type 并仅以其继承的布局命名. slide.metadata.titleSource 说其中哪一个回答. |
⚠️ 未设置
removeNSPrefix: true编辑XMLParser。 。 。 . 它会合并id财务报告和审定财务报表r:id输入一个密钥, 而哪一个胜出取决于源的属性顺序—— 幻灯片顺序 决议似乎在大多数甲板上起作用,而在其他甲板上却沉默地错误地解决。 这个 解析器配置为ignoreAttributes: false财务报告和审定财务报表attributeNamePrefix: "$",以及 已经 没有 脱去前缀. 保持这样.
PowerPoint 解析输出格式规格
该文件为PowerPoint剖析管道(PowerPoint 剖析管道)制作的结构化JSON提供了全面的规格说明pptx 此寄存器中的文件和剪贴板粘贴数据)。 报告综合了以下方面的详细情况:
- 普通中间型(
src/types/normalized.ts(中文(简体) ) - 公共构成部分类型(
src/types/index.ts(中文(简体) ) - 解析器实施
TextParser, (中文(简体) ),ShapeParser, (中文(简体) ),ImageParser, (中文(简体) ),TableParser, (中文(简体) ),DiagramParser, (中文(简体) ),VideoParser, (中文(简体) ),ConnectorParser(中文(简体) ) - 聚合逻辑
PowerPointParser - 现有测试固定装置
test/test-harness/expected/*.json
目的是给消费者一个稳定的合同,说明“平面结果”包含的内容以及如何解释每个领域.
1. 高级别成果组合
存在两个概念层:
- 中级
NormalizedResult) — 内部, z- 顺序 — 剖析时使用的保存结构 . - 解析结果( E)
ParsedResult) — 幻灯片、母版、版式和组件的外部结构PowerPointParser.parseJson(和下游帮手.
取决于您调用哪种公共API(例如,传统包装纸,例如: parsePptx 编号 : parseClipboard 如果重新加入,您通常会收到 ParsedResult- 就像物体或更简单的包装 暴露 slides 阵列.
1.1 解析结果(初级外形)
interface ParsedResult {
slides: ParsedSlide[]; // In presentation order
masters: Record<string, ParsedMaster>; // Keyed by master XML path (ppt/slideMasters/slideMasterX.xml)
layouts: Record<string, ParsedLayout>; // Keyed by layout XML path (ppt/slideLayouts/slideLayoutY.xml)
totalComponents: number; // Total count across all slides (after expansions)
format: 'pptx' | 'clipboard'; // Source format detected by normalizer
slideOrderSource?: 'presentation' | 'filename'; // Whether p:sldIdLst decided the order
slideDimensions?: { width: number; height: number }; // In pixels (EMU converted)
}
1.2 滑动
interface ParsedSlide {
slideIndex: number; // Zero-based position in presentation order
slideNumber: number; // One-based position in presentation order
title?: string; // Title placeholder text, when the slide has one
notes?: string; // Speaker notes, when the slide has any
layoutId?: string; // Derived from layout file name (slideLayoutX)
background?: PowerPointComponent; // Background image/shape (if non-white & accepted)
components: PowerPointComponent[]; // All foreground components, including background IF pushed into flow earlier
metadata: SlideMetadata; // Provenance + counts (see below)
}
interface SlideMetadata {
name: string; // The slide title, or 'Slide N' when it has none
componentCount: number; // components.length
format: 'pptx' | 'clipboard';
slideFile: string | null; // e.g. 'ppt/slides/slide1.xml'
layoutFile: string | null; // e.g. 'ppt/slideLayouts/slideLayout1.xml'
masterFile: string | null; // e.g. 'ppt/slideMasters/slideMaster1.xml'
layoutElementCount: number; // Count of inherited layout elements (pre-filter)
masterElementCount: number; // Count of inherited master elements (pre-filter)
notesFile: string | null; // e.g. 'ppt/notesSlides/notesSlide2.xml'
fileSlideNumber: number | null; // The number in the filename — creation order
titleSource: 'placeholder' | 'layout' | null; // How the title was identified
}
1.3 解析大师
interface ParsedMaster {
id: string; // e.g. 'slideMaster1'
name: string; // 'Master 1'
background?: PowerPointComponent; // Non-white fill or image background
components: PowerPointComponent[]; // Master-level decorative or thematic components
sourceFile: string; // Full master path
placeholders?: PlaceholderMap; // Placeholder geometry by idx or type:*
textStyles?: { // Extracted txStyles for font inheritance
titleStyle?: XMLNode;
bodyStyle?: XMLNode;
};
}
1.4 解析拉尤特
interface ParsedLayout {
id: string; // e.g. 'slideLayout3'
name: string; // 'Layout 3'
masterId?: string; // Associated master id
background?: PowerPointComponent;
components: PowerPointComponent[];
sourceFile: string; // layout file path
placeholders?: PlaceholderMap; // Layout-level placeholders override masters
}
2. 构成部分模式
所有返回的部件都受到歧视 type. 基场:
interface PowerPointComponentBase {
id: string; // Unique within result; generated: <Kind or Name> <OriginalName or Index>
type: ComponentType; // 'text' | 'shape' | 'image' | 'table' | 'video' | 'connection' | 'diagram' | 'any'
content?: string; // Human-readable summary or plain extracted text
x: number; // Pixel position (EMU converted)
y: number;
width: number; // Pixels
height: number; // Pixels
rotation?: number; // Degrees; may be 0; negative supported
style?: ComponentStyle; // See Style section
metadata?: Record<string, any>; // Parser + provenance details
slideIndex: number; // Owning slide zero-based index (masters/layouts use 0 when synthesized)
zIndex: number; // Higher means on top; backgrounds use large negative or normalized values
}
2.1 文本组件
interface TextComponent extends PowerPointComponentBase {
type: 'text';
richText?: RichTextDoc; // TipTap-like structure (paragraphs & bulletLists)
backgroundShape?: { // Only present for text-with-visible background geometry
type: 'rectangle' | 'ellipse' | 'roundRect' | 'custom';
fill?: FillInfo; // { type, color, opacity }
border?: BorderInfo; // { type, color, width, style, cap?, compound? }
geometry?: GeometryInfo; // Normalized geometry description
};
}
关键文本专用元数据字段:
metadata.isTextBox:布尔(来源于cNvSpPr.$txBox(中文(简体) )metadata.paragraphCount编号metadata.hasMultipleRuns:布尔(如果任何段落有多个行)metadata.namespace数字 :'a' | 'p'表示剪贴板对 pptx 源片段
间距高度:当PowerPoint省略它们跨越运行边界时,相邻的字母运行之间可能会合成空格.
2.2 形状组件
interface ShapeComponent extends PowerPointComponentBase {
type: 'shape';
shapeType?: string; // Human-friendly descriptor (e.g. 'rectangle', 'up arrow')
geometry?: GeometryInfo; // { type, preset, isCustom, paths? }
}
样式字段通常为:
style.fillColor, (中文(简体) ),style.fillOpacitystyle.borderColor, (中文(简体) ),style.borderWidth, (中文(简体) ),style.borderStyle- 效果( 如果有的话) 合并成样式: 可能的密钥包括
effects, (中文(简体) ),boxShadow等 (中文(简体) ).
元数据布尔值 :
hasEffects, (中文(简体) ),hasFill, (中文(简体) ),hasBorder
2.3 图像组件
interface ImageComponent extends PowerPointComponentBase {
type: 'image';
src?: string; // Data URL or externally hosted URL (if R2 / remote)
alt?: string; // Description from cNvPr.$descr
}
元数据添加 :
relationshipId: 内在 rIdimageUrl时间:src说明imageType: 已规范扩展( png, jpeg, gif等)imageSize: 字节长度( 如果已知)hasEffects:布尔
注释:
- WMF/EMF图像被跳过(作为装饰背景处理).
- 背景图像仅在中间使用负 Z- 索引; 接受的背景在幻灯片中显示
background或作为流量中的第一个组成部分,取决于处理阶段.
2.4 表格组件
interface TableComponent extends PowerPointComponentBase {
type: 'table';
rows?: { cells: { content: string; style?: ComponentStyle; colSpan?: number; rowSpan?: number }[] }[];
columns?: number; // Count of columns
}
元数据 :
tableData: 二维字符串矩阵( 粗略的文本提取)rows, (中文(简体) ),cols: 数值hasHeader: 当前true如果第一行在richText结构richText: TipTap 表格表达方式反映客户的期望format: 命名空间'a' | 'p'
2.5 图解
interface DiagramComponent extends PowerPointComponentBase {
type: 'diagram';
diagramType?: string; // e.g. 'smartart' or 'unknown'
title?: string; // Derived fallback name
smartArtData?: { // Only for SmartArt when fully extracted
dataPoints: SmartArtDataPoint[]; // Logical model nodes
connections: SmartArtConnection[]; // Logical edges
shapes: SmartArtShape[]; // Visual shape placements
layout: SmartArtLayout; // Layout metadata
};
extractedComponents?: PowerPointComponent[]; // Flattened shapes/text derived from SmartArt
}
若为 options.returnExtractedComponents 在解析时设置,原始 DiagramComponent 包装可被其 extractedComponents 数组( 每个项目为标准) PowerPointComponent 类型 - 经常 shape 或者说 text) (中文(简体) ).
2.6 视频组件
interface VideoComponent extends PowerPointComponentBase {
type: 'video';
url?: string; // Resolved target from slide relationships
thumbnailSrc?: string; // Data URL for poster image (if available)
title?: string; // From cNvPr.$name
embedType?: 'youtube' | 'vimeo' | 'generic';
}
分辨率逻辑区分了PPTX模式(关系XML)和剪贴板模式(simpler关系映射). 如果没有找到匹配的, url 也许 undefined。 。 。 .
2.7 连接组件(连接器/行)
interface ConnectionComponent extends PowerPointComponentBase {
type: 'connection';
startShapeId?: string; // Target shape ID (parsed from connection metadata)
endShapeId?: string;
connectorType?: string; // Derived line archetype
startPoint?: { x: number; y: number }; // Computed anchor point in pixels
endPoint?: { x: number; y: number };
lineStyle?: { // Extracted from <ln>
width?: number; // Pixels
color?: string; // Hex color
dashStyle?: string; // e.g. 'dash'
startArrow?: string; // Arrowhead descriptor
endArrow?: string;
};
}
后处理(fixConnectionPoints) 改进 startPoint 编号 : endPoint 使用存储在 metadata.startConnectionIndex 编号 : metadata.endConnectionIndex 并绑定相联形状的框.
2.8 未知组件
interface UnknownComponent extends PowerPointComponentBase {
type: 'any';
}
当一个解析器无法对一个元素进行分类但仍然返回结构坐标时,很少出现倒置.
3. 样式和可视数据
interface ComponentStyle {
fontFamily?: string;
fontSize?: number; // Points for text; numeric px-like scale after extraction
fontWeight?: string; // 'bold', 'normal', etc.
fontStyle?: string; // 'italic', 'normal'
textAlign?: string; // 'left' | 'center' | 'right' | 'justify'
color?: string; // Foreground text color (hex)
borderWidth?: number; // Pixels
borderStyle?: string; // 'solid' | 'none' | dash patterns
borderColor?: string;
fillColor?: string; // Background fill (shape/table)
fillOpacity?: number; // 0–1 normalized
rotation?: number; // Degrees
// Additional dynamic keys include effects, shadows, filters
[key: string]: any;
}
填充、 边框、 几何辅助工具( 从形状解析) :
interface FillInfo { type: 'solid' | 'gradient' | 'pattern' | 'none'; color: string; opacity: number; }
interface BorderInfo { type: 'solid' | 'none'; color: string; width: number; style: string; cap?: string; compound?: string; }
interface GeometryInfo { type: string; preset: string | null; isCustom: boolean; paths?: any[]; }
影响EffectsInfo - 合并为样式)可能包括:
effects: string[]boxShadow?: string- 其他特定效果键
图像效果增强字段 :
opacity, (中文(简体) ),filter, (中文(简体) ),borderRadius, (中文(简体) ),shadow, (中文(简体) ),effectsList
4. 坐标和单位处理
- PowerPoint内部使用EMUs(英语:Metric Units).
- 全部导出
x, (中文(简体) ),y, (中文(简体) ),width, (中文(简体) ),height转换为 像素 使用集中常数( 以emuToPixels编号 :validatePixelRange) (中文(简体) ). - 旋转由1/60000度单位转换为平平度.
- 幻灯片尺寸( E)
slideDimensions)也作像素转换. - 护栏:任意尺寸 > 50,000会触发警告(可能是未转换的EMU情景).
5. Z-命令和背景逻辑
- 从主机/放电/滑坡中取出的背景非常负面
zIndex在正常化期间:- 主背景:从2000年开始并减少.
- 布局背景:始于-1000并缩小.
- 幻灯片特定背景: 以
zIndex解析时为-500,并可在两者中储存slide.background财务报告和审定财务报表components(取决于舞台) 如果他们通过了非白色的标准.
- 白/相近白固体填充被压制(被打出)为主/排出背景来避免噪音.
- 图像背景总是被保存.
6. 占位符和主样式继承
具有零变形大小/位置的文本和形状组件将查看占位符几何,如果:
- 他们提到占位符(
nvSpPr > nvPr > ph),以及 - 匹配的占位符( E)
idx或者说type)存在于合并的占位符图中(主先是,再是排版覆盖).
主文本样式( T)titleStyle, (中文(简体) ), bodyStyle) 当文本元素缺乏明确性时,影响字体大小继承 rPr 大小申报.
字体大小分辨率的优先排序 :
- 首个有大小的显式非空运行
- 占位符生成主机
titleStyle(如果占位符类型是"标题") - 师父
bodyStyle - 默认倒计时( Arial 18, 或其他参数默认)
7. 丰富的文本代表
所有丰富的文本表面(文本框和表格单元格)均用简化的TipTap类似JSON来表示:
{ type: 'doc', content: Array<Paragraph | BulletList | TableNode> }
支持的节点 :
paragraph- 相继内置text节点bulletList→listItem- 筑巢)paragraphtable(仅限表格) +tableRow→tableCell编号 :tableHeader→paragraph
文本标记( E)marks包括:
{ type: 'bold' }{ type: 'italic' }{ type: 'textStyle', attrs: { fontSize: '22pt', color: '#FFFFFF', ... } }
间距平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平平.
8. 图(SmartArt)扩展
当检测出智能艺术 :
diagramType设置为smartart- 原始关系ID被扫描以收集相伴图/数据/绘图文件.
smartArtData包括:dataPoints: 具有层次和内容的逻辑节点设置( c)connections: 连接数据点的边缘shapes: 代表每个数据节点的定位形状layout: 总结布局 Type, 类别, 颜色Scheme, 快速Style
- 若为
returnExtractedComponents提供了旗帜, 解析器返回平面extractedComponents而不是图包,允许统一下游处理.
9. 连接(线/箭)
连接器依赖 :
startConnection编号 :endConnection从正版 XML (IDs + 连接站点索引)- 使用引用形状的边框来计算像素锚点( 中心、 中左、 中右等) 的第二个通过
- 样式提取从
<ln>(宽,颜色,快活的风格,箭头)
元数据字段协助处理后:
startConnectionIndex, (中文(简体) ),endConnectionIndex- 布尔旗
hasStartConnection, (中文(简体) ),hasEndConnection
10. 媒体和关系解决
图像和视频使用关系图 :
- PPTX 模式: 关系基于文件 (
ppt/slides/_rels/slideN.xml.rels页:1 幻灯片本身的回转部分被明确传递下来。 从幻灯片的位置衍生出来 将会读取另一个幻灯片的关系 一旦一个甲板重新排序. - 剪贴板模式:被幻灯片索引所键入的被简化的地图所存储的关系.
- 背景图像重组
blipFill到地表时embed统一处理的身份证. - 视频缩略图通过嵌入解析
blipFill- > (中文)embed内thumbnailRelationshipId如果现在.
跳过媒体 :
wmf编号 :emf图像(往往是向量占位符或噪音).
11. 背景接受规则
只有在下列情况下才增加候选人背景:
- 这是一个图像,OR
- 它有一个非白/近白填充(固态,梯度,图案).
白种背景被压制以减少冗余,特别是在布局/主继承上,让幻灯片内容显示默认画布背景.
12. 身份证和名称
id 战略用途 BaseParser.generateComponentId(kind, componentIndex, originalName) 确保独特性。 Connectors 附加分号 - 限定片段用于后期形状 ID 提取 。 连接组件 id 格式示例 :
<GeneratedConnectorName>;<UnderlyingShapeId>;<namespace><slideIndex>
连接器引用的形状 ID 从第二个中提取 ; 部分( c)shapeId) (中文(简体) ).
13. 处理错误和 Null 返回
每个专业解析器返回 null 当 :
- 缺少结构节点( E)
spPr, (中文(简体) ),blipFill, (中文(简体) ),graphicData页:1 - 0宽/ 高( 对于形状、 图表)
- 空文本内容( 文本解析器)
- 被过滤的图像类型 (wmf/emf)
集合者默默地忽略 null (可选择在调试模式下登录).
14. 实例
14.1 文本组件(剪贴板)
{
"id": "TextBox 4",
"type": "text",
"content": "TEXT",
"x": 109,
"y": 41,
"width": 288,
"height": 139,
"style": { "fontSize": 80, "color": "#4EA72E", ... },
"richText": { "type": "doc", "content": [ { "type": "paragraph", ... } ] },
"backgroundShape": { "type": "rectangle", "border": { ... }, "geometry": { ... } },
"metadata": { "namespace": "a", "paragraphCount": 1 }
}
14.2 元件组件
{
"id": "Arrow: Up 12",
"type": "shape",
"content": "up arrow shape",
"x": 341,
"y": 471,
"width": 124,
"height": 176,
"style": { "fillColor": "#4472C4", "borderStyle": "none", ... },
"shapeType": "up arrow",
"geometry": { "type": "up arrow", "preset": "upArrow", "isCustom": false },
"metadata": { "hasFill": true, "hasBorder": false }
}
14.3 图像组件(背景)
{
"id": "Google Shape;308;p58",
"type": "image",
"content": "Google Shape;308;p58",
"x": 25,
"y": 53,
"width": 569,
"height": 473,
"src": "data:image/png;base64,...",
"metadata": { "relationshipId": "rId1", "imageType": "png", "imageSize": 220591 }
}
14.4 连接部分
{
"id": "Connector 5;42;p0",
"type": "connection",
"content": "Connector 5 Connection",
"x": 100, "y": 120, "width": 200, "height": 0,
"startShapeId": "1001",
"endShapeId": "1002",
"startPoint": { "x": 300, "y": 140 },
"endPoint": { "x": 100, "y": 140 },
"lineStyle": { "width": 2, "color": "#000000", "dashStyle": "solid" },
"metadata": { "startConnectionIndex": 3, "endConnectionIndex": 1 }
}
(示例通用;实际身份证可能不同. )
15. 版本和稳定性说明
- 受歧视的工会
PowerPointComponent是主要稳定合同. - 新的组成部分类型(例如未来的媒体或互动要素)将以独特的方式扩大结合
type价值和添加剂字段. - 可选元数据字段是添加剂;消费者应当进行特征检测.
- 规范化的内部结构有待完善,但将继续提供相同的外部构件形状.
16. 移徙和兼容性准则
整合时:
- 继续
type用于分枝逻辑. - 使用存在检查 (单位:千美元)
if ('richText' in component)在访问可选字段之前. - 不要假设 背景只出现在
slide.background它们也可能是第一个components[]传统消费者的条目. - 期望数组 喜欢
richText.content财务报告和审定财务报表rows是空的,而不是没有null当不存在数据时. - 忽略未知的密钥 内
style- 未来效应浓缩将出现在那里.
17. 已知限制 / TODO
- SmartArt 提取: 覆盖范围可能不同; 复杂的布局可能会产生部分
smartArtData。 。 。 . - 表格样式:细胞级字体/颜色细节目前最小(结构优先于丰富的格式).
- 视频缩略图:不总是根据关系解析情况提供.
- 接通器:复杂的路由(elbows,曲线)被简化为边框终点;目前还没有路径几何.
- 图表扩展可能返回大型组件集;考虑过滤
extractedComponents下游。 。 .
发展
这个库是更大的PowerPoint剖析生态系统的一部分. 整个系统包括:
- @cliftonc/pptx-to-json (英语): 此解析库
- Web 应用程序: 现场演示和视觉界面
- 云花岗工人: 分析操作的 API 端点
地方发展
# Build the library
npm run build
# Run tests
npm test
# Type checking
npm run type-check
# Test with sample files
npm run log-bin sample.pptx
技术细节
建筑
库使用模块解析器架构 :
- 基本编解器: EMU转换、颜色和变换的通用公用事业
- PowerPointParser 软件: 主要协调员处理格式检测
- 文本编辑器: 有字体/样式解析的文本组件
- 形状: 有填充、边框、效果的几何形状
- 图像处理器: 有数据 URL 提取的图像
- 表格处理器: 有单元格结构和格式的表格
格式支持
- PPTX 文件: 完成办公室打开 XML PowerPoint 文件
- 剪贴板数据: 带有正确命名空间处理的PowerPoint剪贴板格式
- 检测: 可靠的分类和倒置逻辑
许可证
麻省理工学院
捐款
欢迎提问和拉票请求! 请访问 GitHub 仓库 更多信息.
相关项目
- 🌐 现场直播: 尝试浏览器中的解析器
- 📚 文档: 主存储处的全面实施细节
由 Q 制作 克利夫顿·坎宁安