Docx
사용자가 Word 문서(.docx 파일)를 생성, 읽기, 편집 또는 조작하려고 할 때마다 이 기술을 사용하십시오. 트리거에는 'Word 문서', '워드 문서', '.docx'에 대한 언급 또는 목차, 제목, 페이지 번호 또는 레터헤드와 같은 형식의 전문 문서 생성 요청이 포함됩니다. 또한.docx 파일에서 콘텐츠를 추출 또는 재구성할 때, 문서에 이미지를 삽입하거나 바꿀 때, Word 파일에서 찾기 및 바꾸기를 수행할 때, 추적된 변경 내용이나 주석 작업을 할 때, 콘텐츠를 세련된 Word 문서로 변환할 때 사용합니다. 사용자가 '보고서', '메모', '편지', '템플릿' 또는 Word 또는.docx 파일과 유사한 결과물을 요청하는 경우 이 기술을 사용하세요. PDF, 스프레드시트, Google Docs 또는 문서 생성과 관련 없는 일반 코딩 작업에는 사용하지 마세요.
출처: 인류학/기술(MIT)에서 채택한 콘텐츠.
개요
.docx 파일은 XML 파일이 포함된 ZIP 아카이브입니다.
빠른 참조
| 작업 | 접근 |
|---|---|
| 콘텐츠 읽기/분석 | pandoc또는 원시 XML용 압축 풀기 |
| 새 문서 만들기 | docx-js사용 - 아래의 새 문서 만들기 |
| 기존 문서 편집 | 압축 풀기 -> XML 편집 -> 재압축 - 아래의 기존 문서 편집 참조 |
.doc를.docx로 변환
편집하기 전에 레거시.doc파일을 변환해야 합니다.
python scripts/office/soffice.py --headless --convert-to docx document.doc콘텐츠 읽기
# Text extraction with tracked changes
pandoc --track-changes=all document.docx -o output.md
# Raw XML access
python scripts/office/unpack.py document.docx unpacked/이미지로 변환
python scripts/office/soffice.py --headless --convert-to pdf document.docx
pdftoppm -jpeg -r 150 document.pdf page추적된 변경 사항 수락
추적된 모든 변경 사항이 허용된 깨끗한 문서를 생성하려면(LibreOffice 필요):
python scripts/accept_changes.py input.docx output.docx새 문서 만들기
JavaScript로.docx 파일을 생성한 다음 유효성을 검사합니다. 설치:npm install -g docx
설정
const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, ImageRun,
Header, Footer, AlignmentType, PageOrientation, LevelFormat, ExternalHyperlink,
InternalHyperlink, Bookmark, FootnoteReferenceRun, PositionalTab,
PositionalTabAlignment, PositionalTabRelativeTo, PositionalTabLeader,
TabStopType, TabStopPosition, Column, SectionType,
TableOfContents, HeadingLevel, BorderStyle, WidthType, ShadingType,
VerticalAlign, PageNumber, PageBreak } = require('docx');
const doc = new Document({ sections: [{ children: [/* content */] }] });
Packer.toBuffer(doc).then(buffer => fs.writeFileSync("doc.docx", buffer));검증
파일을 생성한 후 유효성을 검사합니다. 유효성 검사에 실패하면 압축을 풀고 XML을 수정한 후 다시 압축하세요.
python scripts/office/validate.py doc.docx페이지 크기
// CRITICAL: docx-js defaults to A4, not US Letter
// Always set page size explicitly for consistent results
sections: [{
properties: {
page: {
size: {
width: 12240, // 8.5 inches in DXA
height: 15840 // 11 inches in DXA
},
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } // 1 inch margins
}
},
children: [/* content */]
}]일반적인 페이지 크기(DXA 단위, 1440 DXA = 1인치):
| 종이 | 폭 | 신장 | 콘텐츠 너비(1" 여백) |
|---|---|---|---|
| 미국 편지 | 12,240 | 15,840 | 9,360 |
| A4(기본값) | 11,906 | 16,838 | 9,026 |
가로 방향: docx-js는 내부적으로 너비/높이를 교환하므로 세로 크기를 전달하고 교환을 처리하도록 합니다.
size: {
width: 12240, // Pass SHORT edge as width
height: 15840, // Pass LONG edge as height
orientation: PageOrientation.LANDSCAPE // docx-js swaps them in the XML
},
// Content width = 15840 - left margin - right margin (uses the long edge)스타일(내장 제목 재정의)
Arial을 기본 글꼴로 사용합니다(범용적으로 지원됨). 가독성을 위해 제목을 검은색으로 유지하세요.
const doc = new Document({
styles: {
default: { document: { run: { font: "Arial", size: 24 } } }, // 12pt default
paragraphStyles: [
// IMPORTANT: Use exact IDs to override built-in styles
{ id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true,
run: { size: 32, bold: true, font: "Arial" },
paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0 } }, // outlineLevel required for TOC
{ id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true,
run: { size: 28, bold: true, font: "Arial" },
paragraph: { spacing: { before: 180, after: 180 }, outlineLevel: 1 } },
]
},
sections: [{
children: [
new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("Title")] }),
]
}]
});목록(유니코드 글머리 기호를 절대 사용하지 마세요)
// WRONG - never manually insert bullet characters
new Paragraph({ children: [new TextRun("* Item")] }) // BAD
new Paragraph({ children: [new TextRun("\u2022 Item")] }) // BAD
// CORRECT - use numbering config with LevelFormat.BULLET
const doc = new Document({
numbering: {
config: [
{ reference: "bullets",
levels: [{ level: 0, format: LevelFormat.BULLET, text: "*", alignment: AlignmentType.LEFT,
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
{ reference: "numbers",
levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT,
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
]
},
sections: [{
children: [
new Paragraph({ numbering: { reference: "bullets", level: 0 },
children: [new TextRun("Bullet item")] }),
new Paragraph({ numbering: { reference: "numbers", level: 0 },
children: [new TextRun("Numbered item")] }),
]
}]
});
// Each reference creates INDEPENDENT numbering
// Same reference = continues (1,2,3 then 4,5,6)
// Different reference = restarts (1,2,3 then 1,2,3)테이블
중요: 테이블에는 이중 너비가 필요합니다 - 테이블에columnWidths를 설정하고 각 셀에width를 모두 설정합니다. 둘 다 없으면 일부 플랫폼에서 테이블이 잘못 렌더링됩니다.
// CRITICAL: Always set table width for consistent rendering
// CRITICAL: Use ShadingType.CLEAR (not SOLID) to prevent black backgrounds
const border = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" };
const borders = { top: border, bottom: border, left: border, right: border };
new Table({
width: { size: 9360, type: WidthType.DXA }, // Always use DXA (percentages break in Google Docs)
columnWidths: [4680, 4680], // Must sum to table width (DXA: 1440 = 1 inch)
rows: [
new TableRow({
children: [
new TableCell({
borders,
width: { size: 4680, type: WidthType.DXA }, // Also set on each cell
shading: { fill: "D5E8F0", type: ShadingType.CLEAR }, // CLEAR not SOLID
margins: { top: 80, bottom: 80, left: 120, right: 120 }, // Cell padding (internal, not added to width)
children: [new Paragraph({ children: [new TextRun("Cell")] })]
})
]
})
]
})테이블 너비 계산:
Google Docs에서는 항상WidthType.DXA-WidthType.PERCENTAGE중단을 사용하세요.
// Table width = sum of columnWidths = content width
// US Letter with 1" margins: 12240 - 2880 = 9360 DXA
width: { size: 9360, type: WidthType.DXA },
columnWidths: [7000, 2360] // Must sum to table width너비 규칙:
- 항상
WidthType.DXA사용 - 절대WidthType.PERCENTAGE사용 안 함(Google Docs와 호환되지 않음) - 테이블 너비는
columnWidths의 합과 같아야 합니다. - 셀
width는 해당columnWidth와 일치해야 합니다. - 셀
margins는 내부 패딩입니다. 셀 너비를 늘리지 않고 콘텐츠 영역을 줄입니다. - 전체 너비 표의 경우: 콘텐츠 너비(페이지 너비에서 왼쪽 및 오른쪽 여백을 뺀 값)를 사용합니다.
이미지
// CRITICAL: type parameter is REQUIRED
new Paragraph({
children: [new ImageRun({
type: "png", // Required: png, jpg, jpeg, gif, bmp, svg
data: fs.readFileSync("image.png"),
transformation: { width: 200, height: 150 },
altText: { title: "Title", description: "Desc", name: "Name" } // All three required
})]
})페이지 나누기
// CRITICAL: PageBreak must be inside a Paragraph
new Paragraph({ children: [new PageBreak()] })
// Or use pageBreakBefore
new Paragraph({ pageBreakBefore: true, children: [new TextRun("New page")] })하이퍼링크
// External link
new Paragraph({
children: [new ExternalHyperlink({
children: [new TextRun({ text: "Click here", style: "Hyperlink" })],
link: "https://example.com",
})]
})
// Internal link (bookmark + reference)
// 1. Create bookmark at destination
new Paragraph({ heading: HeadingLevel.HEADING_1, children: [
new Bookmark({ id: "chapter1", children: [new TextRun("Chapter 1")] }),
]})
// 2. Link to it
new Paragraph({ children: [new InternalHyperlink({
children: [new TextRun({ text: "See Chapter 1", style: "Hyperlink" })],
anchor: "chapter1",
})]})각주
const doc = new Document({
footnotes: {
1: { children: [new Paragraph("Source: Annual Report 2024")] },
2: { children: [new Paragraph("See appendix for methodology")] },
},
sections: [{
children: [new Paragraph({
children: [
new TextRun("Revenue grew 15%"),
new FootnoteReferenceRun(1),
new TextRun(" using adjusted metrics"),
new FootnoteReferenceRun(2),
],
})]
}]
});탭 정지
// Right-align text on same line (e.g., date opposite a title)
new Paragraph({
children: [
new TextRun("Company Name"),
new TextRun("\tJanuary 2025"),
],
tabStops: [{ type: TabStopType.RIGHT, position: TabStopPosition.MAX }],
})
// Dot leader (e.g., TOC-style)
new Paragraph({
children: [
new TextRun("Introduction"),
new TextRun({ children: [
new PositionalTab({
alignment: PositionalTabAlignment.RIGHT,
relativeTo: PositionalTabRelativeTo.MARGIN,
leader: PositionalTabLeader.DOT,
}),
"3",
]}),
],
})다중 열 레이아웃
// Equal-width columns
sections: [{
properties: {
column: {
count: 2, // number of columns
space: 720, // gap between columns in DXA (720 = 0.5 inch)
equalWidth: true,
separate: true, // vertical line between columns
},
},
children: [/* content flows naturally across columns */]
}]
// Custom-width columns (equalWidth must be false)
sections: [{
properties: {
column: {
equalWidth: false,
children: [
new Column({ width: 5400, space: 720 }),
new Column({ width: 3240 }),
],
},
},
children: [/* content */]
}]type: SectionType.NEXT_COLUMN를 사용하여 새 섹션으로 열 나누기를 강제 실행합니다.
목차
// CRITICAL: Headings must use HeadingLevel ONLY - no custom styles
new TableOfContents("Table of Contents", { hyperlink: true, headingStyleRange: "1-3" })머리글/바닥글
sections: [{
properties: {
page: { margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } } // 1440 = 1 inch
},
headers: {
default: new Header({ children: [new Paragraph({ children: [new TextRun("Header")] })] })
},
footers: {
default: new Footer({ children: [new Paragraph({
children: [new TextRun("Page "), new TextRun({ children: [PageNumber.CURRENT] })]
})] })
},
children: [/* content */]
}]docx-js의 중요한 규칙
- 페이지 크기를 명시적으로 설정 - docx-js의 기본값은 A4입니다. 미국 문서에는 US Letter(12240 x 15840 DXA)를 사용합니다.
- 가로: 세로 크기 전달 - docx-js는 내부적으로 너비/높이를 바꿉니다. 짧은 가장자리를
width로, 긴 가장자리를height로 전달하고orientation: PageOrientation.LANDSCAPE를 설정합니다. \n를 절대 사용하지 마세요 - 별도의 단락 요소를 사용하세요- 유니코드 글머리 기호를 사용하지 마세요 - 번호 지정 구성과 함께
LevelFormat.BULLET를 사용하세요. - PageBreak는 단락에 있어야 합니다 - 독립 실행형은 잘못된 XML을 생성합니다.
- ImageRun에는
type가 필요합니다 - 항상 png/jpg/etc를 지정하세요. - 항상 DXA를 사용하여 테이블
width설정 -WidthType.PERCENTAGE를 사용하지 마세요(Google Docs에서 중단됨) - 테이블에는 이중 너비가 필요합니다 -
columnWidths배열 및 셀width, 둘 다 일치해야 함 - 테이블 너비 = 열 너비의 합계 - DXA의 경우 정확하게 합산되었는지 확인하세요.
- 항상 셀 여백을 추가하세요 - 읽을 수 있는 패딩을 위해
margins: { top: 80, bottom: 80, left: 120, right: 120 }를 사용하세요 ShadingType.CLEAR사용 - 테이블 음영 처리에는 절대로 SOLID를 사용하지 마세요.- 테이블을 구분선/규칙으로 사용하지 마세요 - 셀은 최소 높이를 가지며 빈 상자(머리글/바닥글 포함)로 렌더링됩니다. 대신 단락에
border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: "2E75B6", space: 1 } }를 사용하세요. 2열 바닥글의 경우 테이블이 아닌 탭 정지(탭 정지 섹션 참조)를 사용하세요. - TOC에는 HeadingLevel만 필요합니다 - 제목 단락에는 사용자 정의 스타일이 없습니다.
- 내장 스타일 재정의 - 정확한 ID 사용: "제목1", "제목2" 등
outlineLevel포함 - TOC에 필수(H1의 경우 0, H2의 경우 1 등)
기존 문서 편집
3단계를 모두 순서대로 따르세요.
1단계: 포장 풀기
python scripts/office/unpack.py document.docx unpacked/XML을 추출하고, 예쁘게 인쇄하고, 인접한 실행을 병합하고, 둥근 따옴표를 XML 엔터티(“등)로 변환하여 편집이 유지되도록 합니다. 병합 실행을 건너뛰려면--merge-runs false를 사용하세요.
2단계: XML 편집
unpacked/word/에서 파일을 편집합니다. 패턴은 아래 XML 참조를 참조하세요.
사용자가 명시적으로 다른 이름 사용을 요청하지 않는 한 추적된 변경 사항 및 댓글에 "Claude"를 작성자로 사용하세요**.
문자열 교체를 위해 편집 도구를 직접 사용하세요. Python 스크립트를 작성하지 마십시오. 스크립트는 불필요한 복잡성을 초래합니다. 편집 도구는 대체되는 내용을 정확하게 보여줍니다.
중요: 새 콘텐츠에 스마트 따옴표를 사용하세요. 아포스트로피나 따옴표가 포함된 텍스트를 추가할 때 XML 엔터티를 사용하여 스마트 따옴표를 생성하세요.
<!-- Use these entities for professional typography -->
<w:t>Here’s a quote: “Hello”</w:t>| 엔터티 | 캐릭터 |
|---|---|
‘ | '(왼쪽 싱글) |
’ | '(오른쪽 단일/아포스트로피) |
“ | "(왼쪽 이중) |
” | "(오른쪽 이중) |
설명 추가:comment.py를 사용하여 여러 XML 파일의 상용구를 처리합니다(텍스트는 사전 이스케이프 처리된 XML이어야 함).
python scripts/comment.py unpacked/ 0 "Comment text with & and ’"
python scripts/comment.py unpacked/ 1 "Reply text" --parent 0 # reply to comment 0
python scripts/comment.py unpacked/ 0 "Text" --author "Custom Author" # custom author name그런 다음 document.xml에 마커를 추가합니다(XML 참조의 주석 참조).
3단계: 짐 꾸리기
python scripts/office/pack.py unpacked/ output.docx --original document.docx자동 복구로 유효성을 검사하고, XML을 압축하고, DOCX를 생성합니다. 건너뛰려면--validate false를 사용하세요.
자동 복구로 해결되는 문제:
durableId>= 0x7FFFFFFF(유효한 ID 재생성)- 공백이 있는
<w:t>에서xml:space="preserve"가 누락되었습니다.
자동 복구로 문제가 해결되지 않음:
- 잘못된 XML, 잘못된 요소 중첩, 관계 누락, 스키마 위반
일반적인 함정
- 전체
<w:r>요소 교체: 추적된 변경 사항을 추가할 때 전체<w:r>...</w:r>블록을 형제인<w:del>...<w:ins>...로 교체합니다. 실행 내에 추적된 변경 태그를 삽입하지 마세요. <w:rPr>형식 유지: 원본 실행의<w:rPr>블록을 추적된 변경 실행에 복사하여 굵게, 글꼴 크기 등을 유지합니다.
XML 참조
스키마 준수
<w:pPr>의 요소 순서:<w:pStyle>,<w:numPr>,<w:spacing>,<w:ind>,<w:jc>,<w:rPr>마지막- 공백: 선행/후행 공백을 사용하여
xml:space="preserve"를<w:t>에 추가합니다. - RSID: 8자리 16진수여야 합니다(예:
00AB1234).
추적된 변경 사항
삽입:
<w:ins w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z">
<w:r><w:t>inserted text</w:t></w:r>
</w:ins>삭제:
<w:del w:id="2" w:author="Claude" w:date="2025-01-01T00:00:00Z">
<w:r><w:delText>deleted text</w:delText></w:r>
</w:del><w:del>내부:<w:t>대신<w:delText>를 사용하고<w:instrText>대신<w:delInstrText>를 사용합니다.
최소한의 수정 - 변경사항만 표시:
<!-- Change "30 days" to "60 days" -->
<w:r><w:t>The term is </w:t></w:r>
<w:del w:id="1" w:author="Claude" w:date="...">
<w:r><w:delText>30</w:delText></w:r>
</w:del>
<w:ins w:id="2" w:author="Claude" w:date="...">
<w:r><w:t>60</w:t></w:r>
</w:ins>
<w:r><w:t> days.</w:t></w:r>전체 단락/목록 항목 삭제 - 단락에서 모든 콘텐츠를 제거할 때 단락 표시도 삭제됨으로 표시하여 다음 단락과 병합됩니다.<w:pPr><w:rPr>안에<w:del/>를 추가합니다.
<w:p>
<w:pPr>
<w:numPr>...</w:numPr> <!-- list numbering if present -->
<w:rPr>
<w:del w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z"/>
</w:rPr>
</w:pPr>
<w:del w:id="2" w:author="Claude" w:date="2025-01-01T00:00:00Z">
<w:r><w:delText>Entire paragraph content being deleted...</w:delText></w:r>
</w:del>
</w:p><w:pPr><w:rPr>에<w:del/>가 없으면 변경 사항을 적용하면 빈 단락/목록 항목이 남습니다.
다른 저자의 삽입 거부 - 삽입 내에서 삭제 중첩:
<w:ins w:author="Jane" w:id="5">
<w:del w:author="Claude" w:id="10">
<w:r><w:delText>their inserted text</w:delText></w:r>
</w:del>
</w:ins>다른 작성자의 삭제 복원 - 다음에 삽입 추가(삭제 수정 안 함):
<w:del w:author="Jane" w:id="5">
<w:r><w:delText>deleted text</w:delText></w:r>
</w:del>
<w:ins w:author="Claude" w:id="10">
<w:r><w:t>deleted text</w:t></w:r>
</w:ins>댓글
comment.py를 실행한 후(2단계 참조) document.xml에 마커를 추가합니다. 응답하려면 상위 항목 내부에--parent플래그와 중첩 마커를 사용하세요.
중요:<w:commentRangeStart>및<w:commentRangeEnd>는<w:r>의 형제이며 결코<w:r>내부에 있지 않습니다.
<!-- Comment markers are direct children of w:p, never inside w:r -->
<w:commentRangeStart w:id="0"/>
<w:del w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z">
<w:r><w:delText>deleted</w:delText></w:r>
</w:del>
<w:r><w:t> more text</w:t></w:r>
<w:commentRangeEnd w:id="0"/>
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="0"/></w:r>
<!-- Comment 0 with reply 1 nested inside -->
<w:commentRangeStart w:id="0"/>
<w:commentRangeStart w:id="1"/>
<w:r><w:t>text</w:t></w:r>
<w:commentRangeEnd w:id="1"/>
<w:commentRangeEnd w:id="0"/>
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="0"/></w:r>
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="1"/></w:r>이미지
word/media/에 이미지 파일 추가word/_rels/document.xml.rels에 관계 추가:
<Relationship Id="rId5" Type=".../image" Target="media/image1.png"/>[Content_Types].xml에 콘텐츠 유형을 추가합니다.
<Default Extension="png" ContentType="image/png"/>- document.xml의 참조:
<w:drawing>
<wp:inline>
<wp:extent cx="914400" cy="914400"/> <!-- EMUs: 914400 = 1 inch -->
<a:graphic>
<a:graphicData uri=".../picture">
<pic:pic>
<pic:blipFill><a:blip r:embed="rId5"/></pic:blipFill>
</pic:pic>
</a:graphicData>
</a:graphic>
</wp:inline>
</w:drawing>종속성
- pandoc: 텍스트 추출
- docx:
npm install -g docx(새 문서) - LibreOffice: PDF 변환(
scripts/office/soffice.py를 통해 샌드박스 환경에 대해 자동 구성) - 포플러: 이미지용
pdftoppm
리소스 파일
라이센스.txt
바이너리 리소스
스크립트/init.py
스크립트/accept_changes.py
"""Accept all tracked changes in a DOCX file using LibreOffice.
Requires LibreOffice (soffice) to be installed.
"""
import argparse
import logging
import shutil
import subprocess
from pathlib import Path
from office.soffice import get_soffice_env
logger = logging.getLogger(__name__)
LIBREOFFICE_PROFILE = "/tmp/libreoffice_docx_profile"
MACRO_DIR = f"{LIBREOFFICE_PROFILE}/user/basic/Standard"
ACCEPT_CHANGES_MACRO = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE script:module PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "module.dtd">
<script:module xmlns:script="http://openoffice.org/2000/script" script:name="Module1" script:language="StarBasic">
Sub AcceptAllTrackedChanges()
Dim document As Object
Dim dispatcher As Object
document = ThisComponent.CurrentController.Frame
dispatcher = createUnoService("com.sun.star.frame.DispatchHelper")
dispatcher.executeDispatch(document, ".uno:AcceptAllTrackedChanges", "", 0, Array())
ThisComponent.store()
ThisComponent.close(True)
End Sub
</script:module>"""
def accept_changes(
input_file: str,
output_file: str,
) -> tuple[None, str]:
input_path = Path(input_file)
output_path = Path(output_file)
if not input_path.exists():
return None, f"Error: Input file not found: {input_file}"
if not input_path.suffix.lower() == ".docx":
return None, f"Error: Input file is not a DOCX file: {input_file}"
try:
output_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(input_path, output_path)
except Exception as e:
return None, f"Error: Failed to copy input file to output location: {e}"
if not _setup_libreoffice_macro():
return None, "Error: Failed to setup LibreOffice macro"
cmd = [
"soffice",
"--headless",
f"-env:UserInstallation=file://{LIBREOFFICE_PROFILE}",
"--norestore",
"vnd.sun.star.script:Standard.Module1.AcceptAllTrackedChanges?language=Basic&location=application",
str(output_path.absolute()),
]
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=30,
check=False,
env=get_soffice_env(),
)
except subprocess.TimeoutExpired:
return (
None,
f"Successfully accepted all tracked changes: {input_file} -> {output_file}",
)
if result.returncode != 0:
return None, f"Error: LibreOffice failed: {result.stderr}"
return (
None,
f"Successfully accepted all tracked changes: {input_file} -> {output_file}",
)
def _setup_libreoffice_macro() -> bool:
macro_dir = Path(MACRO_DIR)
macro_file = macro_dir / "Module1.xba"
if macro_file.exists() and "AcceptAllTrackedChanges" in macro_file.read_text():
return True
if not macro_dir.exists():
subprocess.run(
[
"soffice",
"--headless",
f"-env:UserInstallation=file://{LIBREOFFICE_PROFILE}",
"--terminate_after_init",
],
capture_output=True,
timeout=10,
check=False,
env=get_soffice_env(),
)
macro_dir.mkdir(parents=True, exist_ok=True)
try:
macro_file.write_text(ACCEPT_CHANGES_MACRO)
return True
except Exception as e:
logger.warning(f"Failed to setup LibreOffice macro: {e}")
return False
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Accept all tracked changes in a DOCX file"
)
parser.add_argument("input_file", help="Input DOCX file with tracked changes")
parser.add_argument(
"output_file", help="Output DOCX file (clean, no tracked changes)"
)
args = parser.parse_args()
_, message = accept_changes(args.input_file, args.output_file)
print(message)
if "Error" in message:
raise SystemExit(1)스크립트/comment.py
바이너리 리소스
스크립트/사무실/helpers/init.py
스크립트/office/helpers/init.py 다운로드
바이너리 리소스
스크립트/사무실/helpers/merge_runs.py
스크립트/office/helpers/merge_runs.py 다운로드
"""Merge adjacent runs with identical formatting in DOCX.
Merges adjacent <w:r> elements that have identical <w:rPr> properties.
Works on runs in paragraphs and inside tracked changes (<w:ins>, <w:del>).
Also:
- Removes rsid attributes from runs (revision metadata that doesn't affect rendering)
- Removes proofErr elements (spell/grammar markers that block merging)
"""
from pathlib import Path
import defusedxml.minidom
def merge_runs(input_dir: str) -> tuple[int, str]:
doc_xml = Path(input_dir) / "word" / "document.xml"
if not doc_xml.exists():
return 0, f"Error: {doc_xml} not found"
try:
dom = defusedxml.minidom.parseString(doc_xml.read_text(encoding="utf-8"))
root = dom.documentElement
_remove_elements(root, "proofErr")
_strip_run_rsid_attrs(root)
containers = {run.parentNode for run in _find_elements(root, "r")}
merge_count = 0
for container in containers:
merge_count += _merge_runs_in(container)
doc_xml.write_bytes(dom.toxml(encoding="UTF-8"))
return merge_count, f"Merged {merge_count} runs"
except Exception as e:
return 0, f"Error: {e}"
def _find_elements(root, tag: str) -> list:
results = []
def traverse(node):
if node.nodeType == node.ELEMENT_NODE:
name = node.localName or node.tagName
if name == tag or name.endswith(f":{tag}"):
results.append(node)
for child in node.childNodes:
traverse(child)
traverse(root)
return results
def _get_child(parent, tag: str):
for child in parent.childNodes:
if child.nodeType == child.ELEMENT_NODE:
name = child.localName or child.tagName
if name == tag or name.endswith(f":{tag}"):
return child
return None
def _get_children(parent, tag: str) -> list:
results = []
for child in parent.childNodes:
if child.nodeType == child.ELEMENT_NODE:
name = child.localName or child.tagName
if name == tag or name.endswith(f":{tag}"):
results.append(child)
return results
def _is_adjacent(elem1, elem2) -> bool:
node = elem1.nextSibling
while node:
if node == elem2:
return True
if node.nodeType == node.ELEMENT_NODE:
return False
if node.nodeType == node.TEXT_NODE and node.data.strip():
return False
node = node.nextSibling
return False
def _remove_elements(root, tag: str):
for elem in _find_elements(root, tag):
if elem.parentNode:
elem.parentNode.removeChild(elem)
def _strip_run_rsid_attrs(root):
for run in _find_elements(root, "r"):
for attr in list(run.attributes.values()):
if "rsid" in attr.name.lower():
run.removeAttribute(attr.name)
def _merge_runs_in(container) -> int:
merge_count = 0
run = _first_child_run(container)
while run:
while True:
next_elem = _next_element_sibling(run)
if next_elem and _is_run(next_elem) and _can_merge(run, next_elem):
_merge_run_content(run, next_elem)
container.removeChild(next_elem)
merge_count += 1
else:
break
_consolidate_text(run)
run = _next_sibling_run(run)
return merge_count
def _first_child_run(container):
for child in container.childNodes:
if child.nodeType == child.ELEMENT_NODE and _is_run(child):
return child
return None
def _next_element_sibling(node):
sibling = node.nextSibling
while sibling:
if sibling.nodeType == sibling.ELEMENT_NODE:
return sibling
sibling = sibling.nextSibling
return None
def _next_sibling_run(node):
sibling = node.nextSibling
while sibling:
if sibling.nodeType == sibling.ELEMENT_NODE:
if _is_run(sibling):
return sibling
sibling = sibling.nextSibling
return None
def _is_run(node) -> bool:
name = node.localName or node.tagName
return name == "r" or name.endswith(":r")
def _can_merge(run1, run2) -> bool:
rpr1 = _get_child(run1, "rPr")
rpr2 = _get_child(run2, "rPr")
if (rpr1 is None) != (rpr2 is None):
return False
if rpr1 is None:
return True
return rpr1.toxml() == rpr2.toxml()
def _merge_run_content(target, source):
for child in list(source.childNodes):
if child.nodeType == child.ELEMENT_NODE:
name = child.localName or child.tagName
if name != "rPr" and not name.endswith(":rPr"):
target.appendChild(child)
def _consolidate_text(run):
t_elements = _get_children(run, "t")
for i in range(len(t_elements) - 1, 0, -1):
curr, prev = t_elements[i], t_elements[i - 1]
if _is_adjacent(prev, curr):
prev_text = prev.firstChild.data if prev.firstChild else ""
curr_text = curr.firstChild.data if curr.firstChild else ""
merged = prev_text + curr_text
if prev.firstChild:
prev.firstChild.data = merged
else:
prev.appendChild(run.ownerDocument.createTextNode(merged))
if merged.startswith(" ") or merged.endswith(" "):
prev.setAttribute("xml:space", "preserve")
elif prev.hasAttribute("xml:space"):
prev.removeAttribute("xml:space")
run.removeChild(curr)스크립트/office/helpers/simplify_redlines.py
스크립트/office/helpers/simplify_redlines.py 다운로드
"""Simplify tracked changes by merging adjacent w:ins or w:del elements.
Merges adjacent <w:ins> elements from the same author into a single element.
Same for <w:del> elements. This makes heavily-redlined documents easier to
work with by reducing the number of tracked change wrappers.
Rules:
- Only merges w:ins with w:ins, w:del with w:del (same element type)
- Only merges if same author (ignores timestamp differences)
- Only merges if truly adjacent (only whitespace between them)
"""
import xml.etree.ElementTree as ET
import zipfile
from pathlib import Path
import defusedxml.minidom
WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
def simplify_redlines(input_dir: str) -> tuple[int, str]:
doc_xml = Path(input_dir) / "word" / "document.xml"
if not doc_xml.exists():
return 0, f"Error: {doc_xml} not found"
try:
dom = defusedxml.minidom.parseString(doc_xml.read_text(encoding="utf-8"))
root = dom.documentElement
merge_count = 0
containers = _find_elements(root, "p") + _find_elements(root, "tc")
for container in containers:
merge_count += _merge_tracked_changes_in(container, "ins")
merge_count += _merge_tracked_changes_in(container, "del")
doc_xml.write_bytes(dom.toxml(encoding="UTF-8"))
return merge_count, f"Simplified {merge_count} tracked changes"
except Exception as e:
return 0, f"Error: {e}"
def _merge_tracked_changes_in(container, tag: str) -> int:
merge_count = 0
tracked = [
child
for child in container.childNodes
if child.nodeType == child.ELEMENT_NODE and _is_element(child, tag)
]
if len(tracked) < 2:
return 0
i = 0
while i < len(tracked) - 1:
curr = tracked[i]
next_elem = tracked[i + 1]
if _can_merge_tracked(curr, next_elem):
_merge_tracked_content(curr, next_elem)
container.removeChild(next_elem)
tracked.pop(i + 1)
merge_count += 1
else:
i += 1
return merge_count
def _is_element(node, tag: str) -> bool:
name = node.localName or node.tagName
return name == tag or name.endswith(f":{tag}")
def _get_author(elem) -> str:
author = elem.getAttribute("w:author")
if not author:
for attr in elem.attributes.values():
if attr.localName == "author" or attr.name.endswith(":author"):
return attr.value
return author
def _can_merge_tracked(elem1, elem2) -> bool:
if _get_author(elem1) != _get_author(elem2):
return False
node = elem1.nextSibling
while node and node != elem2:
if node.nodeType == node.ELEMENT_NODE:
return False
if node.nodeType == node.TEXT_NODE and node.data.strip():
return False
node = node.nextSibling
return True
def _merge_tracked_content(target, source):
while source.firstChild:
child = source.firstChild
source.removeChild(child)
target.appendChild(child)
def _find_elements(root, tag: str) -> list:
results = []
def traverse(node):
if node.nodeType == node.ELEMENT_NODE:
name = node.localName or node.tagName
if name == tag or name.endswith(f":{tag}"):
results.append(node)
for child in node.childNodes:
traverse(child)
traverse(root)
return results
def get_tracked_change_authors(doc_xml_path: Path) -> dict[str, int]:
if not doc_xml_path.exists():
return {}
try:
tree = ET.parse(doc_xml_path)
root = tree.getroot()
except ET.ParseError:
return {}
namespaces = {"w": WORD_NS}
author_attr = f"{{{WORD_NS}}}author"
authors: dict[str, int] = {}
for tag in ["ins", "del"]:
for elem in root.findall(f".//w:{tag}", namespaces):
author = elem.get(author_attr)
if author:
authors[author] = authors.get(author, 0) + 1
return authors
def _get_authors_from_docx(docx_path: Path) -> dict[str, int]:
try:
with zipfile.ZipFile(docx_path, "r") as zf:
if "word/document.xml" not in zf.namelist():
return {}
with zf.open("word/document.xml") as f:
tree = ET.parse(f)
root = tree.getroot()
namespaces = {"w": WORD_NS}
author_attr = f"{{{WORD_NS}}}author"
authors: dict[str, int] = {}
for tag in ["ins", "del"]:
for elem in root.findall(f".//w:{tag}", namespaces):
author = elem.get(author_attr)
if author:
authors[author] = authors.get(author, 0) + 1
return authors
except (zipfile.BadZipFile, ET.ParseError):
return {}
def infer_author(modified_dir: Path, original_docx: Path, default: str = "Claude") -> str:
modified_xml = modified_dir / "word" / "document.xml"
modified_authors = get_tracked_change_authors(modified_xml)
if not modified_authors:
return default
original_authors = _get_authors_from_docx(original_docx)
new_changes: dict[str, int] = {}
for author, count in modified_authors.items():
original_count = original_authors.get(author, 0)
diff = count - original_count
if diff > 0:
new_changes[author] = diff
if not new_changes:
return default
if len(new_changes) == 1:
return next(iter(new_changes))
raise ValueError(
f"Multiple authors added new changes: {new_changes}. "
"Cannot infer which author to validate."
)스크립트/office/pack.py
"""Pack a directory into a DOCX, PPTX, or XLSX file.
Validates with auto-repair, condenses XML formatting, and creates the Office file.
Usage:
python pack.py <input_directory> <output_file> [--original <file>] [--validate true|false]
Examples:
python pack.py unpacked/ output.docx --original input.docx
python pack.py unpacked/ output.pptx --validate false
"""
import argparse
import sys
import shutil
import tempfile
import zipfile
from pathlib import Path
import defusedxml.minidom
from validators import DOCXSchemaValidator, PPTXSchemaValidator, RedliningValidator
def pack(
input_directory: str,
output_file: str,
original_file: str | None = None,
validate: bool = True,
infer_author_func=None,
) -> tuple[None, str]:
input_dir = Path(input_directory)
output_path = Path(output_file)
suffix = output_path.suffix.lower()
if not input_dir.is_dir():
return None, f"Error: {input_dir} is not a directory"
if suffix not in {".docx", ".pptx", ".xlsx"}:
return None, f"Error: {output_file} must be a .docx, .pptx, or .xlsx file"
if validate and original_file:
original_path = Path(original_file)
if original_path.exists():
success, output = _run_validation(
input_dir, original_path, suffix, infer_author_func
)
if output:
print(output)
if not success:
return None, f"Error: Validation failed for {input_dir}"
with tempfile.TemporaryDirectory() as temp_dir:
temp_content_dir = Path(temp_dir) / "content"
shutil.copytree(input_dir, temp_content_dir)
for pattern in ["*.xml", "*.rels"]:
for xml_file in temp_content_dir.rglob(pattern):
_condense_xml(xml_file)
output_path.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
for f in temp_content_dir.rglob("*"):
if f.is_file():
zf.write(f, f.relative_to(temp_content_dir))
return None, f"Successfully packed {input_dir} to {output_file}"
def _run_validation(
unpacked_dir: Path,
original_file: Path,
suffix: str,
infer_author_func=None,
) -> tuple[bool, str | None]:
output_lines = []
validators = []
if suffix == ".docx":
author = "Claude"
if infer_author_func:
try:
author = infer_author_func(unpacked_dir, original_file)
except ValueError as e:
print(f"Warning: {e} Using default author 'Claude'.", file=sys.stderr)
validators = [
DOCXSchemaValidator(unpacked_dir, original_file),
RedliningValidator(unpacked_dir, original_file, author=author),
]
elif suffix == ".pptx":
validators = [PPTXSchemaValidator(unpacked_dir, original_file)]
if not validators:
return True, None
total_repairs = sum(v.repair() for v in validators)
if total_repairs:
output_lines.append(f"Auto-repaired {total_repairs} issue(s)")
success = all(v.validate() for v in validators)
if success:
output_lines.append("All validations PASSED!")
return success, "\n".join(output_lines) if output_lines else None
def _condense_xml(xml_file: Path) -> None:
try:
with open(xml_file, encoding="utf-8") as f:
dom = defusedxml.minidom.parse(f)
for element in dom.getElementsByTagName("*"):
if element.tagName.endswith(":t"):
continue
for child in list(element.childNodes):
if (
child.nodeType == child.TEXT_NODE
and child.nodeValue
and child.nodeValue.strip() == ""
) or child.nodeType == child.COMMENT_NODE:
element.removeChild(child)
xml_file.write_bytes(dom.toxml(encoding="UTF-8"))
except Exception as e:
print(f"ERROR: Failed to parse {xml_file.name}: {e}", file=sys.stderr)
raise
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Pack a directory into a DOCX, PPTX, or XLSX file"
)
parser.add_argument("input_directory", help="Unpacked Office document directory")
parser.add_argument("output_file", help="Output Office file (.docx/.pptx/.xlsx)")
parser.add_argument(
"--original",
help="Original file for validation comparison",
)
parser.add_argument(
"--validate",
type=lambda x: x.lower() == "true",
default=True,
metavar="true|false",
help="Run validation with auto-repair (default: true)",
)
args = parser.parse_args()
_, message = pack(
args.input_directory,
args.output_file,
original_file=args.original,
validate=args.validate,
)
print(message)
if "Error" in message:
sys.exit(1)scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd
스크립트/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd 다운로드
바이너리 리소스
scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart드로잉.xsd
scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart드로잉.xsd 다운로드
바이너리 리소스
스크립트/사무실/스키마/ISO-IEC29500-4_2016/dml-diagram.xsd
스크립트/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd 다운로드
바이너리 리소스
scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd
스크립트/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd 다운로드
바이너리 리소스
스크립트/사무실/스키마/ISO-IEC29500-4_2016/dml-main.xsd
스크립트/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd 다운로드
바이너리 리소스
스크립트/사무실/스키마/ISO-IEC29500-4_2016/dml-picture.xsd
스크립트/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd 다운로드
바이너리 리소스
scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheet드로잉.xsd
scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheet드로잉.xsd 다운로드
바이너리 리소스
scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessing드로잉.xsd
scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessing드로잉.xsd 다운로드
바이너리 리소스
스크립트/사무실/스키마/ISO-IEC29500-4_2016/pml.xsd
스크립트/office/schemas/ISO-IEC29500-4_2016/pml.xsd 다운로드
바이너리 리소스
scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd
스크립트/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd 다운로드
바이너리 리소스
scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd
스크립트/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd 다운로드
바이너리 리소스
scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd
스크립트/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd 다운로드
바이너리 리소스
scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd
스크립트/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd 다운로드
바이너리 리소스
scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd
스크립트/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd 다운로드
바이너리 리소스
scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd
스크립트/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd 다운로드
바이너리 리소스
scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd
스크립트/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd 다운로드
바이너리 리소스
scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd
스크립트/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd 다운로드
바이너리 리소스
scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd
스크립트/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd 다운로드
바이너리 리소스
scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd
스크립트/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd 다운로드
바이너리 리소스
스크립트/사무실/스키마/ISO-IEC29500-4_2016/sml.xsd
스크립트/office/schemas/ISO-IEC29500-4_2016/sml.xsd 다운로드
바이너리 리소스
스크립트/사무실/스키마/ISO-IEC29500-4_2016/vml-main.xsd
스크립트/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd 다운로드
바이너리 리소스
scripts/office/schemas/ISO-IEC29500-4_2016/vml-office드로잉.xsd
스크립트/office/schemas/ISO-IEC29500-4_2016/vml-office드로잉.xsd 다운로드
바이너리 리소스
scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentation드로잉.xsd
[scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentation Drawing.xsd 다운로드](/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentation Drawing.xsd)
바이너리 리소스
scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheet드로잉.xsd
scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheet드로잉.xsd 다운로드
바이너리 리소스
scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessing드로잉.xsd
scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessing드로잉.xsd 다운로드
바이너리 리소스
스크립트/사무실/스키마/ISO-IEC29500-4_2016/wml.xsd
스크립트/office/schemas/ISO-IEC29500-4_2016/wml.xsd 다운로드
바이너리 리소스
스크립트/사무실/스키마/ISO-IEC29500-4_2016/xml.xsd
스크립트/office/schemas/ISO-IEC29500-4_2016/xml.xsd 다운로드
바이너리 리소스
스크립트/사무실/스키마/ecma/fouth-edition/opc-contentTypes.xsd
스크립트/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd 다운로드
바이너리 리소스
스크립트/사무실/스키마/ecma/fouth-edition/opc-coreProperties.xsd
스크립트/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd 다운로드
바이너리 리소스
스크립트/사무실/스키마/ecma/fouth-edition/opc-digSig.xsd
스크립트/office/schemas/ecma/fouth-edition/opc-digSig.xsd 다운로드
바이너리 리소스
스크립트/office/schemas/ecma/fouth-edition/opc-relationships.xsd
스크립트/office/schemas/ecma/fouth-edition/opc-relationships.xsd 다운로드
바이너리 리소스
스크립트/사무실/스키마/mce/mc.xsd
스크립트/office/schemas/mce/mc.xsd 다운로드
바이너리 리소스
스크립트/사무실/스키마/microsoft/wml-2010.xsd
스크립트/office/schemas/microsoft/wml-2010.xsd 다운로드
바이너리 리소스
스크립트/사무실/스키마/microsoft/wml-2012.xsd
스크립트/office/schemas/microsoft/wml-2012.xsd 다운로드
바이너리 리소스
스크립트/사무실/스키마/microsoft/wml-2018.xsd
스크립트/office/schemas/microsoft/wml-2018.xsd 다운로드
바이너리 리소스
스크립트/사무실/스키마/microsoft/wml-cex-2018.xsd
스크립트/office/schemas/microsoft/wml-cex-2018.xsd 다운로드
바이너리 리소스
스크립트/사무실/스키마/microsoft/wml-cid-2016.xsd
스크립트/office/schemas/microsoft/wml-cid-2016.xsd 다운로드
바이너리 리소스
스크립트/사무실/스키마/microsoft/wml-sdtdatahash-2020.xsd
스크립트/office/schemas/microsoft/wml-sdtdatahash-2020.xsd 다운로드
바이너리 리소스
스크립트/사무실/스키마/microsoft/wml-symex-2015.xsd
스크립트/office/schemas/microsoft/wml-symex-2015.xsd 다운로드
바이너리 리소스
스크립트/office/soffice.py
"""
Helper for running LibreOffice (soffice) in environments where AF_UNIX
sockets may be blocked (e.g., sandboxed VMs). Detects the restriction
at runtime and applies an LD_PRELOAD shim if needed.
Usage:
from office.soffice import run_soffice, get_soffice_env
# Option 1 – run soffice directly
result = run_soffice(["--headless", "--convert-to", "pdf", "input.docx"])
# Option 2 – get env dict for your own subprocess calls
env = get_soffice_env()
subprocess.run(["soffice", ...], env=env)
"""
import os
import socket
import subprocess
import tempfile
from pathlib import Path
def get_soffice_env() -> dict:
env = os.environ.copy()
env["SAL_USE_VCLPLUGIN"] = "svp"
if _needs_shim():
shim = _ensure_shim()
env["LD_PRELOAD"] = str(shim)
return env
def run_soffice(args: list[str], **kwargs) -> subprocess.CompletedProcess:
env = get_soffice_env()
return subprocess.run(["soffice"] + args, env=env, **kwargs)
_SHIM_SO = Path(tempfile.gettempdir()) / "lo_socket_shim.so"
def _needs_shim() -> bool:
try:
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.close()
return False
except OSError:
return True
def _ensure_shim() -> Path:
if _SHIM_SO.exists():
return _SHIM_SO
src = Path(tempfile.gettempdir()) / "lo_socket_shim.c"
src.write_text(_SHIM_SOURCE)
subprocess.run(
["gcc", "-shared", "-fPIC", "-o", str(_SHIM_SO), str(src), "-ldl"],
check=True,
capture_output=True,
)
src.unlink()
return _SHIM_SO
_SHIM_SOURCE = r"""
#define _GNU_SOURCE
#include <dlfcn.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>
static int (*real_socket)(int, int, int);
static int (*real_socketpair)(int, int, int, int[2]);
static int (*real_listen)(int, int);
static int (*real_accept)(int, struct sockaddr *, socklen_t *);
static int (*real_close)(int);
static int (*real_read)(int, void *, size_t);
/* Per-FD bookkeeping (FDs >= 1024 are passed through unshimmed). */
static int is_shimmed[1024];
static int peer_of[1024];
static int wake_r[1024]; /* accept() blocks reading this */
static int wake_w[1024]; /* close() writes to this */
static int listener_fd = -1; /* FD that received listen() */
__attribute__((constructor))
static void init(void) {
real_socket = dlsym(RTLD_NEXT, "socket");
real_socketpair = dlsym(RTLD_NEXT, "socketpair");
real_listen = dlsym(RTLD_NEXT, "listen");
real_accept = dlsym(RTLD_NEXT, "accept");
real_close = dlsym(RTLD_NEXT, "close");
real_read = dlsym(RTLD_NEXT, "read");
for (int i = 0; i < 1024; i++) {
peer_of[i] = -1;
wake_r[i] = -1;
wake_w[i] = -1;
}
}
/* ---- socket ---------------------------------------------------------- */
int socket(int domain, int type, int protocol) {
if (domain == AF_UNIX) {
int fd = real_socket(domain, type, protocol);
if (fd >= 0) return fd;
/* socket(AF_UNIX) blocked – fall back to socketpair(). */
int sv[2];
if (real_socketpair(domain, type, protocol, sv) == 0) {
if (sv[0] >= 0 && sv[0] < 1024) {
is_shimmed[sv[0]] = 1;
peer_of[sv[0]] = sv[1];
int wp[2];
if (pipe(wp) == 0) {
wake_r[sv[0]] = wp[0];
wake_w[sv[0]] = wp[1];
}
}
return sv[0];
}
errno = EPERM;
return -1;
}
return real_socket(domain, type, protocol);
}
/* ---- listen ---------------------------------------------------------- */
int listen(int sockfd, int backlog) {
if (sockfd >= 0 && sockfd < 1024 && is_shimmed[sockfd]) {
listener_fd = sockfd;
return 0;
}
return real_listen(sockfd, backlog);
}
/* ---- accept ---------------------------------------------------------- */
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) {
if (sockfd >= 0 && sockfd < 1024 && is_shimmed[sockfd]) {
/* Block until close() writes to the wake pipe. */
if (wake_r[sockfd] >= 0) {
char buf;
real_read(wake_r[sockfd], &buf, 1);
}
errno = ECONNABORTED;
return -1;
}
return real_accept(sockfd, addr, addrlen);
}
/* ---- close ----------------------------------------------------------- */
int close(int fd) {
if (fd >= 0 && fd < 1024 && is_shimmed[fd]) {
int was_listener = (fd == listener_fd);
is_shimmed[fd] = 0;
if (wake_w[fd] >= 0) { /* unblock accept() */
char c = 0;
write(wake_w[fd], &c, 1);
real_close(wake_w[fd]);
wake_w[fd] = -1;
}
if (wake_r[fd] >= 0) { real_close(wake_r[fd]); wake_r[fd] = -1; }
if (peer_of[fd] >= 0) { real_close(peer_of[fd]); peer_of[fd] = -1; }
if (was_listener)
_exit(0); /* conversion done – exit */
}
return real_close(fd);
}
"""
if __name__ == "__main__":
import sys
result = run_soffice(sys.argv[1:])
sys.exit(result.returncode)스크립트/office/unpack.py
"""Unpack Office files (DOCX, PPTX, XLSX) for editing.
Extracts the ZIP archive, pretty-prints XML files, and optionally:
- Merges adjacent runs with identical formatting (DOCX only)
- Simplifies adjacent tracked changes from same author (DOCX only)
Usage:
python unpack.py <office_file> <output_dir> [options]
Examples:
python unpack.py document.docx unpacked/
python unpack.py presentation.pptx unpacked/
python unpack.py document.docx unpacked/ --merge-runs false
"""
import argparse
import sys
import zipfile
from pathlib import Path
import defusedxml.minidom
from helpers.merge_runs import merge_runs as do_merge_runs
from helpers.simplify_redlines import simplify_redlines as do_simplify_redlines
SMART_QUOTE_REPLACEMENTS = {
"\u201c": "“",
"\u201d": "”",
"\u2018": "‘",
"\u2019": "’",
}
def unpack(
input_file: str,
output_directory: str,
merge_runs: bool = True,
simplify_redlines: bool = True,
) -> tuple[None, str]:
input_path = Path(input_file)
output_path = Path(output_directory)
suffix = input_path.suffix.lower()
if not input_path.exists():
return None, f"Error: {input_file} does not exist"
if suffix not in {".docx", ".pptx", ".xlsx"}:
return None, f"Error: {input_file} must be a .docx, .pptx, or .xlsx file"
try:
output_path.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(input_path, "r") as zf:
zf.extractall(output_path)
xml_files = list(output_path.rglob("*.xml")) + list(output_path.rglob("*.rels"))
for xml_file in xml_files:
_pretty_print_xml(xml_file)
message = f"Unpacked {input_file} ({len(xml_files)} XML files)"
if suffix == ".docx":
if simplify_redlines:
simplify_count, _ = do_simplify_redlines(str(output_path))
message += f", simplified {simplify_count} tracked changes"
if merge_runs:
merge_count, _ = do_merge_runs(str(output_path))
message += f", merged {merge_count} runs"
for xml_file in xml_files:
_escape_smart_quotes(xml_file)
return None, message
except zipfile.BadZipFile:
return None, f"Error: {input_file} is not a valid Office file"
except Exception as e:
return None, f"Error unpacking: {e}"
def _pretty_print_xml(xml_file: Path) -> None:
try:
content = xml_file.read_text(encoding="utf-8")
dom = defusedxml.minidom.parseString(content)
xml_file.write_bytes(dom.toprettyxml(indent=" ", encoding="utf-8"))
except Exception:
pass
def _escape_smart_quotes(xml_file: Path) -> None:
try:
content = xml_file.read_text(encoding="utf-8")
for char, entity in SMART_QUOTE_REPLACEMENTS.items():
content = content.replace(char, entity)
xml_file.write_text(content, encoding="utf-8")
except Exception:
pass
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Unpack an Office file (DOCX, PPTX, XLSX) for editing"
)
parser.add_argument("input_file", help="Office file to unpack")
parser.add_argument("output_directory", help="Output directory")
parser.add_argument(
"--merge-runs",
type=lambda x: x.lower() == "true",
default=True,
metavar="true|false",
help="Merge adjacent runs with identical formatting (DOCX only, default: true)",
)
parser.add_argument(
"--simplify-redlines",
type=lambda x: x.lower() == "true",
default=True,
metavar="true|false",
help="Merge adjacent tracked changes from same author (DOCX only, default: true)",
)
args = parser.parse_args()
_, message = unpack(
args.input_file,
args.output_directory,
merge_runs=args.merge_runs,
simplify_redlines=args.simplify_redlines,
)
print(message)
if "Error" in message:
sys.exit(1)스크립트/사무실/validate.py
"""
Command line tool to validate Office document XML files against XSD schemas and tracked changes.
Usage:
python validate.py <path> [--original <original_file>] [--auto-repair] [--author NAME]
The first argument can be either:
- An unpacked directory containing the Office document XML files
- A packed Office file (.docx/.pptx/.xlsx) which will be unpacked to a temp directory
Auto-repair fixes:
- paraId/durableId values that exceed OOXML limits
- Missing xml:space="preserve" on w:t elements with whitespace
"""
import argparse
import sys
import tempfile
import zipfile
from pathlib import Path
from validators import DOCXSchemaValidator, PPTXSchemaValidator, RedliningValidator
def main():
parser = argparse.ArgumentParser(description="Validate Office document XML files")
parser.add_argument(
"path",
help="Path to unpacked directory or packed Office file (.docx/.pptx/.xlsx)",
)
parser.add_argument(
"--original",
required=False,
default=None,
help="Path to original file (.docx/.pptx/.xlsx). If omitted, all XSD errors are reported and redlining validation is skipped.",
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="Enable verbose output",
)
parser.add_argument(
"--auto-repair",
action="store_true",
help="Automatically repair common issues (hex IDs, whitespace preservation)",
)
parser.add_argument(
"--author",
default="Claude",
help="Author name for redlining validation (default: Claude)",
)
args = parser.parse_args()
path = Path(args.path)
assert path.exists(), f"Error: {path} does not exist"
original_file = None
if args.original:
original_file = Path(args.original)
assert original_file.is_file(), f"Error: {original_file} is not a file"
assert original_file.suffix.lower() in [".docx", ".pptx", ".xlsx"], (
f"Error: {original_file} must be a .docx, .pptx, or .xlsx file"
)
file_extension = (original_file or path).suffix.lower()
assert file_extension in [".docx", ".pptx", ".xlsx"], (
f"Error: Cannot determine file type from {path}. Use --original or provide a .docx/.pptx/.xlsx file."
)
if path.is_file() and path.suffix.lower() in [".docx", ".pptx", ".xlsx"]:
temp_dir = tempfile.mkdtemp()
with zipfile.ZipFile(path, "r") as zf:
zf.extractall(temp_dir)
unpacked_dir = Path(temp_dir)
else:
assert path.is_dir(), f"Error: {path} is not a directory or Office file"
unpacked_dir = path
match file_extension:
case ".docx":
validators = [
DOCXSchemaValidator(unpacked_dir, original_file, verbose=args.verbose),
]
if original_file:
validators.append(
RedliningValidator(unpacked_dir, original_file, verbose=args.verbose, author=args.author)
)
case ".pptx":
validators = [
PPTXSchemaValidator(unpacked_dir, original_file, verbose=args.verbose),
]
case _:
print(f"Error: Validation not supported for file type {file_extension}")
sys.exit(1)
if args.auto_repair:
total_repairs = sum(v.repair() for v in validators)
if total_repairs:
print(f"Auto-repaired {total_repairs} issue(s)")
success = all(v.validate() for v in validators)
if success:
print("All validations PASSED!")
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()스크립트/사무실/검증기/init.py
스크립트/office/validators/init.py 다운로드
"""
Validation modules for Word document processing.
"""
from .base import BaseSchemaValidator
from .docx import DOCXSchemaValidator
from .pptx import PPTXSchemaValidator
from .redlining import RedliningValidator
__all__ = [
"BaseSchemaValidator",
"DOCXSchemaValidator",
"PPTXSchemaValidator",
"RedliningValidator",
]스크립트/사무실/검증기/base.py
스크립트/office/validators/base.py 다운로드
바이너리 리소스
스크립트/office/validators/docx.py
스크립트 다운로드/office/validators/docx.py
바이너리 리소스
스크립트/사무실/검증기/pptx.py
스크립트 다운로드/office/validators/pptx.py
바이너리 리소스
스크립트/사무실/검증기/redlining.py
스크립트 다운로드/office/validators/redlining.py
바이너리 리소스
스크립트/템플릿/comments.xml
바이너리 리소스
스크립트/템플릿/commentsExtended.xml
스크립트/템플릿/commentsExtended.xml 다운로드
바이너리 리소스
스크립트/템플릿/commentsExtensible.xml
스크립트/템플릿/commentsExtensible.xml 다운로드
바이너리 리소스
스크립트/템플릿/commentsIds.xml
바이너리 리소스
스크립트/템플릿/people.xml
바이너리 리소스
GitHub에서 보기
문서 공동 작성
문서 공동 작성을 위한 구조화된 워크플로를 사용자에게 안내합니다. 사용자가 문서, 제안서, 기술 사양, 의사 결정 문서 또는 유사한 구조의 콘텐츠를 작성하려고 할 때 사용합니다. 이 워크플로는 사용자가 효율적으로 컨텍스트를 전달하고, 반복을 통해 콘텐츠를 구체화하고, 문서가 독자에게 적합한지 확인하는 데 도움이 됩니다. 사용자가 문서 작성, 제안서 작성, 사양 초안 작성 또는 유사한 문서 작업을 언급할 때 트리거됩니다.
사용자가 PDF 파일로 무엇이든 하고 싶을 때마다 이 기술을 사용하세요. 여기에는 PDF에서 텍스트/표 읽기 또는 추출, 여러 PDF를 하나로 결합 또는 병합, PDF 분할, 페이지 회전, 워터마크 추가, 새 PDF 생성, PDF 양식 채우기, PDF 암호화/암호 해독, 이미지 추출 및 스캔한 PDF에서 OCR을 포함하여 검색 가능하게 만듭니다. 사용자가.pdf 파일을 언급하거나 파일 생성을 요청하는 경우 이 기술을 사용하세요.
클로데스킬스 문서