diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..e3b414c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "semi": false, + "singleQuote": true, + "trailingComma": "all" +} diff --git a/packages/parser/src/__tests__/parse.test.ts b/packages/parser/src/__tests__/parse.test.ts index c1b6526..03be909 100644 --- a/packages/parser/src/__tests__/parse.test.ts +++ b/packages/parser/src/__tests__/parse.test.ts @@ -34,7 +34,7 @@ describe('parse', () => { }) it('should parse collect sectionNumber', () => { - assert.strictEqual(hwpDocument.info.sectionSize, 1) + assert.strictEqual(hwpDocument.info.properties.sections, 1) }) it('should be collect page size', () => { diff --git a/packages/parser/src/doc-info-parser.ts b/packages/parser/src/doc-info-parser.ts deleted file mode 100644 index eabde9d..0000000 --- a/packages/parser/src/doc-info-parser.ts +++ /dev/null @@ -1,320 +0,0 @@ -/** - * Copyright Han Lee and other contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { CFB$Container, find } from 'cfb' -import { inflate } from 'pako' - -import { FillType } from './constants/fill-type.js' -import { DocInfoTagID } from './constants/tag-id.js' -import { BinData, BinDataCompress } from './models/doc-info/bin-data.js' -import { ByteReader } from './utils/byte-reader.js' -import { CharShape } from './models/doc-info/char-shape.js' -import { DocInfo } from './models/doc-info/doc-info.js' -import { FontFace } from './models/doc-info/font-face.js' -import { ParagraphShape } from './models/doc-info/paragraph-shape.js' -import { getRGB, getFlag, getBitValue } from './utils/bit-utils.js' -import { BorderFill } from './models/doc-info/border-fill.js' -import { HWPRecord } from './models/record.js' -import { Panose } from './models/doc-info/panose.js' -import { HWPHeader } from './models/header.js' -import { PeekableIterator } from './utils/generator.js' -import { collectChildren } from './utils/record.js' - -export class DocInfoParser { - private reader: ByteReader - - private result = new DocInfo() - - private container: CFB$Container - - private header: HWPHeader - - constructor(header: HWPHeader, data: Uint8Array, container: CFB$Container) { - this.header = header - this.reader = new ByteReader(data.buffer) - this.container = container - } - - visitDocumentProperties(record: HWPRecord) { - const reader = new ByteReader(record.data) - this.result.sectionSize = reader.readUInt16() - - this.result.startingIndex.page = reader.readUInt16() - this.result.startingIndex.footnote = reader.readUInt16() - this.result.startingIndex.endnote = reader.readUInt16() - this.result.startingIndex.picture = reader.readUInt16() - this.result.startingIndex.table = reader.readUInt16() - this.result.startingIndex.equation = reader.readUInt16() - - this.result.caratLocation.listId = reader.readUInt32() - this.result.caratLocation.paragraphId = reader.readUInt32() - this.result.caratLocation.charIndex = reader.readUInt32() - } - - visitCharShape(record: HWPRecord) { - const reader = new ByteReader(record.data) - - const charShape = new CharShape( - [ - reader.readUInt16(), - reader.readUInt16(), - reader.readUInt16(), - reader.readUInt16(), - reader.readUInt16(), - reader.readUInt16(), - reader.readUInt16(), - ], - [ - reader.readUInt8(), - reader.readUInt8(), - reader.readUInt8(), - reader.readUInt8(), - reader.readUInt8(), - reader.readUInt8(), - reader.readUInt8(), - ], - [ - reader.readInt8(), - reader.readInt8(), - reader.readInt8(), - reader.readInt8(), - reader.readInt8(), - reader.readInt8(), - reader.readInt8(), - ], - [ - reader.readUInt8(), - reader.readUInt8(), - reader.readUInt8(), - reader.readUInt8(), - reader.readUInt8(), - reader.readUInt8(), - reader.readUInt8(), - ], - [ - reader.readInt8(), - reader.readInt8(), - reader.readInt8(), - reader.readInt8(), - reader.readInt8(), - reader.readInt8(), - reader.readInt8(), - ], - reader.readInt32(), - reader.readUInt32(), - reader.readUInt8(), - reader.readUInt8(), - reader.readUInt32(), - reader.readUInt32(), - reader.readUInt32(), - reader.readUInt32(), - ) - - if (record.data.byteLength > 68) { - charShape.fontBackgroundId = reader.readUInt16() - } - - if (record.data.byteLength > 70) { - charShape.underLineColor = getRGB(reader.readInt32()) - } - - this.result.charShapes.push(charShape) - } - - visitFaceName(record: HWPRecord) { - const reader = new ByteReader(record.data) - const attribute = reader.readUInt8() - const hasAlternative = getFlag(attribute, 7) - const hasAttribute = getFlag(attribute, 6) - const hasDefault = getFlag(attribute, 5) - - const fontFace = new FontFace() - fontFace.name = reader.readString() - - if (hasAlternative) { - reader.skipByte(1) - fontFace.alternative = reader.readString() - } - - if (hasAttribute) { - const panose = new Panose() - panose.family = reader.readInt8() - panose.serifStyle = reader.readInt8() - panose.weight = reader.readInt8() - panose.proportion = reader.readInt8() - panose.contrast = reader.readInt8() - panose.strokeVariation = reader.readInt8() - panose.armStyle = reader.readInt8() - panose.letterForm = reader.readInt8() - panose.midline = reader.readInt8() - panose.xHeight = reader.readInt8() - - fontFace.panose = panose - } - - if (hasDefault) { - fontFace.default = reader.readString() - } - - this.result.fontFaces.push(fontFace) - } - - visitBinData(record: HWPRecord) { - const reader = new ByteReader(record.data) - // TODO: (@hahnlee) parse properties - const attribute = reader.readUInt16() - - const properties = { - type: getBitValue(attribute, 0, 3), - compress: getBitValue(attribute, 4, 5), - status: getBitValue(attribute, 8, 9), - } - - const id = reader.readUInt16() - const extension = reader.readString() - - // FIXME: (@hanlee) check embed - const path = `Root Entry/BinData/BIN${`${id.toString(16).toUpperCase()}`.padStart(4, '0')}.${extension}` - const payload = find(this.container, path)!.content - - if ( - properties.compress === BinDataCompress.COMPRESS - || (properties.compress === BinDataCompress.DEFAULT && this.header.flags.compressed) - ) { - const data = inflate(Uint8Array.from(payload), { windowBits: -15 }) - this.result.binData.push(new BinData(properties, extension, data)) - } else { - this.result.binData.push(new BinData(properties, extension, Uint8Array.from(payload))) - } - } - - visitBorderFill(record: HWPRecord) { - const reader = new ByteReader(record.data) - - const borderFill = new BorderFill( - reader.readUInt16(), - { - left: { - type: reader.readUInt8(), - width: reader.readUInt8(), - color: getRGB(reader.readUInt32()), - }, - right: { - type: reader.readUInt8(), - width: reader.readUInt8(), - color: getRGB(reader.readUInt32()), - }, - top: { - type: reader.readUInt8(), - width: reader.readUInt8(), - color: getRGB(reader.readUInt32()), - }, - bottom: { - type: reader.readUInt8(), - width: reader.readUInt8(), - color: getRGB(reader.readUInt32()), - }, - }, - ) - - reader.skipByte(6) - - if (reader.readUInt32() === FillType.Single) { - borderFill.backgroundColor = getRGB(reader.readUInt32()) - } - - this.result.borderFills.push(borderFill) - } - - visitParagraphShape(record: HWPRecord) { - const reader = new ByteReader(record.data) - const attribute = reader.readUInt32() - - const shape = new ParagraphShape() - shape.align = getBitValue(attribute, 2, 4) - this.result.paragraphShapes.push(shape) - } - - visitCompatibleDocument(record: HWPRecord) { - const reader = new ByteReader(record.data) - this.result.compatibleDocument = reader.readUInt32() - } - - visitLayoutCompatibility(record: HWPRecord) { - const reader = new ByteReader(record.data) - this.result.layoutCompatibility.char = reader.readUInt32() - this.result.layoutCompatibility.paragraph = reader.readUInt32() - this.result.layoutCompatibility.section = reader.readUInt32() - this.result.layoutCompatibility.object = reader.readUInt32() - this.result.layoutCompatibility.field = reader.readUInt32() - } - - private visit = (record: HWPRecord, iterator: PeekableIterator) => { - switch (record.id) { - case DocInfoTagID.HWPTAG_DOCUMENT_PROPERTIES: { - this.visitDocumentProperties(record) - break - } - - case DocInfoTagID.HWPTAG_CHAR_SHAPE: { - this.visitCharShape(record) - break - } - - case DocInfoTagID.HWPTAG_FACE_NAME: { - this.visitFaceName(record) - break - } - - case DocInfoTagID.HWPTAG_BIN_DATA: { - this.visitBinData(record) - break - } - - case DocInfoTagID.HWPTAG_BORDER_FILL: { - this.visitBorderFill(record) - break - } - - case DocInfoTagID.HWPTAG_PARA_SHAPE: { - this.visitParagraphShape(record) - break - } - - case DocInfoTagID.HWPTAG_COMPATIBLE_DOCUMENT: { - this.visitCompatibleDocument(record) - break - } - - case DocInfoTagID.HWPTAG_LAYOUT_COMPATIBILITY: { - this.visitLayoutCompatibility(record) - break - } - - default: - break - } - - collectChildren(iterator, record.level).forEach(record => this.visit(record, iterator)) - } - - parse() { - const iterator = new PeekableIterator(this.reader.records()) - const record = iterator.next() - this.visit(record, iterator) - return this.result - } -} diff --git a/packages/parser/src/models/controls/element-properties.ts b/packages/parser/src/models/controls/element-properties.ts index ec39730..8b07f5f 100644 --- a/packages/parser/src/models/controls/element-properties.ts +++ b/packages/parser/src/models/controls/element-properties.ts @@ -191,7 +191,8 @@ export class Outline { const attribute = reader.readUInt32() const kind = mapBorderKind(getBitValue(attribute, 0, 5)) - const defaultCap = ctrlId === CommonCtrlID.Picture ? EndCap.Round : EndCap.Flat + const defaultCap = + ctrlId === CommonCtrlID.Picture ? EndCap.Round : EndCap.Flat const endCap = mapEndCap(getBitValue(attribute, 6, 9)) || defaultCap const headStyle = mapArrowStyle(getBitValue(attribute, 10, 15)) const tailStyle = mapArrowStyle(getBitValue(attribute, 16, 21)) diff --git a/packages/parser/src/models/controls/shapes/picture.ts b/packages/parser/src/models/controls/shapes/picture.ts index eb03866..91d165b 100644 --- a/packages/parser/src/models/controls/shapes/picture.ts +++ b/packages/parser/src/models/controls/shapes/picture.ts @@ -247,7 +247,9 @@ export class PictureEffect { static fromReader(reader: ByteReader) { const attribute = reader.readUInt32() - const shadow = getFlag(attribute, 0) ? PictureShadow.fromReader(reader) : null + const shadow = getFlag(attribute, 0) + ? PictureShadow.fromReader(reader) + : null const glow = getFlag(attribute, 1) ? Glow.fromReader(reader) : null const softEdge = getFlag(attribute, 2) ? SoftEdge.fromReader(reader) : null diff --git a/packages/parser/src/models/doc-info/bin-data.ts b/packages/parser/src/models/doc-info/bin-data.ts index 5305565..3d60be0 100644 --- a/packages/parser/src/models/doc-info/bin-data.ts +++ b/packages/parser/src/models/doc-info/bin-data.ts @@ -14,41 +14,138 @@ * limitations under the License. */ -export enum BinDataType { - LINK, - EMBEDDING, - STORAGE, +import { DocInfoTagID } from '../../constants/tag-id.js' +import { getBitValue } from '../../utils/bit-utils.js' +import { ByteReader } from '../../utils/byte-reader.js' +import { HWPHeader } from '../header.js' +import type { HWPRecord } from '../record.js' + +export enum BinDataKind { + /** 그림 외부 파일 참조 */ + Link, + /** 그림 파일 포함 */ + Embedding, + /** OLE 포함 */ + Storage, } -export enum BinDataCompress { - DEFAULT, - COMPRESS, - NOT_COMPRESS, +export enum CompressMode { + /** 스토리지의 디폴트 모드 따라감 */ + Default, + /** 무조건 압축 */ + Compress, + /** 무조건 압축하지 않음 */ + None, } export enum BinDataStatus { - INITIAL, - SUCCESS, - ERROR, - IGNORE, + /** 아직 access 된 적이 없는 상태 */ + Initial, + /** access에 성공하여 파일을 찾은 상태 */ + Success, + /** access가 실패한 에러 상태 */ + Failed, + /** 링크 access가 실패했으나 무시된 상태 */ + Ignored, } -interface BinProperties { - type: BinDataType - compress: BinDataCompress - status: BinDataStatus +export class BinDataProperties { + constructor( + /** 타입 */ + public kind: BinDataKind, + /** 압축 모드 */ + public compressMode: CompressMode, + /** 상태 */ + public status: BinDataStatus, + ) {} + + static fromBits(bits: number) { + return new BinDataProperties( + mapBinDataKind(getBitValue(bits, 0, 3)), + mapCompressMode(getBitValue(bits, 4, 5)), + mapBinDataStatus(getBitValue(bits, 8, 9)), + ) + } } export class BinData { - properties: BinProperties + constructor(public properties: BinDataProperties) {} + + public absolutePath?: string + public relativePath?: string + public id?: number + public extension?: string + + static fromRecord(record: HWPRecord) { + if (record.id !== DocInfoTagID.HWPTAG_BIN_DATA) { + throw new Error('DocInfo: BinData: Record has wrong ID') + } + + const reader = new ByteReader(record.data) + const properties = BinDataProperties.fromBits(reader.readUInt16()) + + const binData = new BinData(properties) + if (properties.kind === BinDataKind.Link) { + binData.absolutePath = reader.readString() + binData.relativePath = reader.readString() + } + + if (properties.kind === BinDataKind.Embedding) { + binData.id = reader.readUInt16() + binData.extension = reader.readString() + } + + if (properties.kind === BinDataKind.Storage) { + binData.id = reader.readUInt16() + } + + if (!reader.isEOF()) { + throw new Error('DocInfo: BinData: Reader is not EOF') + } + + return binData + } - extension: string + getCFBFileName() { + if (this.properties.kind !== BinDataKind.Embedding) { + return null + } - payload: Uint8Array + const extension = this.extension?.toLowerCase() + const id = this.id?.toString(16).padStart(4, '0') + + return `BIN${id}.${extension}` + } + + public compressed(header: HWPHeader) { + switch (this.properties.compressMode) { + case CompressMode.Default: + return header.flags.compressed + case CompressMode.Compress: + return true + case CompressMode.None: + return false + } + } +} + +function mapBinDataKind(value: number) { + if (value >= BinDataKind.Link && value <= BinDataKind.Storage) { + return value as BinDataKind + } + throw new Error(`Unknown BinDataKind: ${value}`) +} + +function mapCompressMode(value: number) { + if (value >= CompressMode.Default && value <= CompressMode.None) { + return value as CompressMode + } + throw new Error(`Unknown CompressMode: ${value}`) +} - constructor(properties: BinProperties, extension: string, payload: Uint8Array) { - this.properties = properties - this.extension = extension - this.payload = payload +function mapBinDataStatus(value: number) { + if (value >= BinDataStatus.Initial && value <= BinDataStatus.Ignored) { + return value as BinDataStatus } + throw new Error(`Unknown BinDataStatus: ${value}`) } diff --git a/packages/parser/src/models/doc-info/border-fill.ts b/packages/parser/src/models/doc-info/border-fill.ts index 6249293..4117ec3 100644 --- a/packages/parser/src/models/doc-info/border-fill.ts +++ b/packages/parser/src/models/doc-info/border-fill.ts @@ -14,40 +14,139 @@ * limitations under the License. */ -import { RGB } from '../../types/color.js' +import { DocInfoTagID } from '../../constants/tag-id.js' +import { getBitValue, getFlag } from '../../utils/bit-utils.js' import { ByteReader } from '../../utils/byte-reader.js' import { ColorRef } from '../color-ref.js' +import { HWPRecord } from '../record.js' import { Image } from './bullet.js' -interface BorerStyle { - type: number - width: number - color: RGB -} +export class BorderFill { + constructor( + /** 3D 효과의 유무 */ + public effect3d: boolean, + /** 그림자 효과의 유무 */ + public effectShadow: boolean, + /** Slash 대각선 모양 */ + public slashDiagonalShape: SlashDiagonalShape, + /** BackSlash 대각선 모양 */ + public backSlashDiagonalShape: number, + /** Slash 대각선 꺾은선 여부 */ + public brokenSlashDiagonalLine: boolean, + /** BackSlash 대각선 꺾은선 여부 */ + public brokenBackSlashDiagonalLine: boolean, + /** Slash 대각선 모양 180도 회전 여부 */ + public slackDiagonalLineRotated: boolean, + /** BackSlash 대각선 모양 180도 회전 여부 */ + public backSlackDiagonalLineRotated: boolean, + /** 중심선 유무 */ + public centerLine: boolean, + /** 선 정보 */ + public borders: [Border, Border, Border, Border], + /** 대각선 */ + public diagonalBorder: Border, + /** 채우기 정보 */ + public fill: Fill, + ) {} + + static fromRecord(record: HWPRecord) { + if (record.id !== DocInfoTagID.HWPTAG_BORDER_FILL) { + throw new Error('DocInfo: Border: Record has wrong ID') + } + + const reader = new ByteReader(record.data) + const attributes = reader.readUInt16() + + const effect3d = getFlag(attributes, 0) + const effectShadow = getFlag(attributes, 1) + const slashDiagonalShape = mapSlashDiagonalShape( + getBitValue(attributes, 2, 4), + ) + const backSlashDiagonalShape = mapBackSlashDiagonalShape( + getBitValue(attributes, 5, 7), + ) + const brokenSlashDiagonalLine = getBitValue(attributes, 8, 9) > 0 + const brokenBackSlashDiagonalLine = getFlag(attributes, 10) + const slackDiagonalLineRotated = getFlag(attributes, 11) + const backSlackDiagonalLineRotated = getFlag(attributes, 12) + const centerLine = getFlag(attributes, 13) + const borders: [Border, Border, Border, Border] = [ + Border.fromReader(reader), + Border.fromReader(reader), + Border.fromReader(reader), + Border.fromReader(reader), + ] + const diagonalBorder = Border.fromReader(reader) + const fill = mapFill(reader) + + if (!reader.isEOF()) { + throw new Error('DocInfo: Border: Reader is not EOF') + } -export interface BorderFillStyle { - left: BorerStyle - right: BorerStyle - top: BorerStyle - bottom: BorerStyle + return new BorderFill( + effect3d, + effectShadow, + slashDiagonalShape, + backSlashDiagonalShape, + brokenSlashDiagonalLine, + brokenBackSlashDiagonalLine, + slackDiagonalLineRotated, + backSlackDiagonalLineRotated, + centerLine, + borders, + diagonalBorder, + fill, + ) + } } -export class BorderFill { - // TODO: (@hahnlee) getter & setter 만들기 - attribute: number +export enum SlashDiagonalShape { + None = 0b000, + Slash = 0b010, + LeftTopToBottomEdge = 0b011, + LeftTopToRightEdge = 0b110, + LeftTopToBottomRightEdge = 0b111, +} - style: BorderFillStyle +function mapSlashDiagonalShape(value: number) { + switch (value) { + case SlashDiagonalShape.None: + return SlashDiagonalShape.None + case SlashDiagonalShape.Slash: + return SlashDiagonalShape.Slash + case SlashDiagonalShape.LeftTopToBottomEdge: + return SlashDiagonalShape.LeftTopToBottomEdge + case SlashDiagonalShape.LeftTopToRightEdge: + return SlashDiagonalShape.LeftTopToRightEdge + case SlashDiagonalShape.LeftTopToBottomRightEdge: + return SlashDiagonalShape.LeftTopToBottomRightEdge + default: + throw new Error(`Unknown slash diagonal shape: ${value}`) + } +} - // TODO: (@hahnlee) 그라데이션도 처리하기 - backgroundColor: RGB | null = null +export enum BackSlashDiagonalShape { + None = 0b000, + BackSlash = 0b010, + RightTopToBottomEdge = 0b011, + RightTopToLeftEdge = 0b110, + RightTopToBottomLeftEdge = 0b111, +} - constructor( - attribute: number, - style: BorderFillStyle, - ) { - this.attribute = attribute - this.style = style +function mapBackSlashDiagonalShape(value: number) { + switch (value) { + case BackSlashDiagonalShape.None: + return BackSlashDiagonalShape.None + case BackSlashDiagonalShape.BackSlash: + return BackSlashDiagonalShape.BackSlash + case BackSlashDiagonalShape.RightTopToBottomEdge: + return BackSlashDiagonalShape.RightTopToBottomEdge + case BackSlashDiagonalShape.RightTopToLeftEdge: + return BackSlashDiagonalShape.RightTopToLeftEdge + case BackSlashDiagonalShape.RightTopToBottomLeftEdge: + return BackSlashDiagonalShape.RightTopToBottomLeftEdge } + throw new Error(`Unknown back slash diagonal shape: ${value}`) } export class Border { diff --git a/packages/parser/src/models/doc-info/bullet.ts b/packages/parser/src/models/doc-info/bullet.ts index c58e21e..0503b01 100644 --- a/packages/parser/src/models/doc-info/bullet.ts +++ b/packages/parser/src/models/doc-info/bullet.ts @@ -17,6 +17,7 @@ import { DocInfoTagID } from '../../constants/tag-id.js' import { ByteReader } from '../../utils/byte-reader.js' import type { HWPRecord } from '../record.js' +import { HWPVersion } from '../version.js' import { ParagraphHead } from './numbering.js' export class Bullet { @@ -33,7 +34,7 @@ export class Bullet { public checkedChar: string, ) {} - static fromRecord(record: HWPRecord) { + static fromRecord(record: HWPRecord, version: HWPVersion) { if (record.id !== DocInfoTagID.HWPTAG_BULLET) { throw new Error('DocInfo: Bullet: Record has wrong ID') } @@ -42,9 +43,13 @@ export class Bullet { const paragraphHead = ParagraphHead.fromReader(reader, false) const bulletChar = String.fromCharCode(reader.readUInt16()) + const useImage = reader.readUInt32() > 0 const image = Image.fromReader(reader) - const checkedChar = String.fromCharCode(reader.readUInt16()) + + const checkedChar = reader.isEOF() + ? '' + : String.fromCharCode(reader.readUInt16()) if (!reader.isEOF()) { throw new Error('DocInfo: Bullet: Reader is not EOF') diff --git a/packages/parser/src/models/doc-info/change-tracking-author.ts b/packages/parser/src/models/doc-info/change-tracking-author.ts new file mode 100644 index 0000000..c54b8e8 --- /dev/null +++ b/packages/parser/src/models/doc-info/change-tracking-author.ts @@ -0,0 +1,30 @@ +/** + * Copyright Han Lee and other contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DocInfoTagID } from '../../constants/tag-id.js' +import type { HWPRecord } from '../record.js' + +export class ChangeTrackingAuthor { + constructor() {} + + static fromRecord(record: HWPRecord) { + if (record.id !== DocInfoTagID.HWPTAG_TRACK_CHANGE_AUTHOR) { + throw new Error('DocInfo: ChangeTrackingAuthor: Record has wrong ID') + } + + return new ChangeTrackingAuthor() + } +} diff --git a/packages/parser/src/models/doc-info/change-tracking.ts b/packages/parser/src/models/doc-info/change-tracking.ts new file mode 100644 index 0000000..3464a33 --- /dev/null +++ b/packages/parser/src/models/doc-info/change-tracking.ts @@ -0,0 +1,30 @@ +/** + * Copyright Han Lee and other contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DocInfoTagID } from '../../constants/tag-id.js' +import type { HWPRecord } from '../record.js' + +export class ChangeTracking { + static fromRecord(record: HWPRecord) { + if (record.id !== DocInfoTagID.HWPTAG_TRACK_CHANGE) { + throw new Error('DocInfo: ChangeTracking: Record has wrong ID') + } + + // TODO: implement + + return new ChangeTracking() + } +} diff --git a/packages/parser/src/models/doc-info/char-shape.ts b/packages/parser/src/models/doc-info/char-shape.ts index 8aadb33..5227561 100644 --- a/packages/parser/src/models/doc-info/char-shape.ts +++ b/packages/parser/src/models/doc-info/char-shape.ts @@ -14,69 +14,290 @@ * limitations under the License. */ -import { getRGB } from '../../utils/bit-utils.js' -import { RGB } from '../../types/color.js' +import { getBitValue, getFlag } from '../../utils/bit-utils.js' +import { ColorRef } from '../color-ref.js' +import type { HWPRecord } from '../record.js' +import { HWPVersion } from '../version.js' +import { BorderKind, mapBorderKind } from './border-fill.js' +import { ByteReader } from '../../utils/byte-reader.js' +import { DocInfoTagID } from '../../constants/tag-id.js' -type SupportedLocaleOptions = [number, number, number, number, number, number, number] +type SupportedLocaleOptions = [ + number, + number, + number, + number, + number, + number, + number, +] -export class CharShape { - fontId: SupportedLocaleOptions +export class CharShapeStyle { + constructor( + /** 언어별 글꼴 ID(FaceID) 참조 값 */ + public fontIds: SupportedLocaleOptions, + /** 언어별 장평, 50%~200% */ + public fontScales: SupportedLocaleOptions, + /** 언어별 자간 */ + public fontSpacings: SupportedLocaleOptions, + /** 언어별 상대 크기, 10%~250% */ + public fontSizes: SupportedLocaleOptions, + /** 언어별 글자 위치, -100%~100% */ + public fontPositions: SupportedLocaleOptions, + /** 기준 크기, 0pt~4096pt */ + public fontBaseSize: number, + /** 기울임 여부 */ + public italic: boolean, + /** 진하게 여부 */ + public bold: boolean, + /** 밑줄 종류 */ + public underlineKind: UnderlineKind, + /** 밑줄 모양 */ + public underlineShape: BorderKind, + /** 외곽선 종류 */ + public outlineKind: OutlineKind, + /** 그림자 종류 */ + public shadowKind: CharShadowKind, + /** 양각여부 */ + public emboss: boolean, + /** 음각여부 */ + public engrave: boolean, + /** 위 첨자 여부 */ + public supscript: boolean, + /** 아래 첨자 여부 */ + public subscript: boolean, + /** 취소선 여부 */ + public strike: boolean, + /** 강조점 종류 */ + public symMark: SymMark, + /** 글꼴에 어울리는 빈칸 사용 여부 */ + public useFontSpace: boolean, + /** 취소선 모양 */ + public strikeShape: BorderKind, + /** Kerning 여부 */ + public useKerning: boolean, + /** 그림자 간격 X, -100%~100% */ + public shadowOffsetX: number, + /** 그림자 간격 Y, -100%~100% */ + public shadowOffsetY: number, + /** 글자 색 */ + public color: ColorRef, + /** 밑줄 색 */ + public underlineColor: ColorRef, + /** 음영 색 */ + public shadeColor: ColorRef, + /** 그림자 색 */ + public shadowColor: ColorRef, + ) {} + /** 글자 테두리/배경ID 참조 값 (5.0.2.1 이상) */ + public borderFillId?: number + /** 취소선 색 (5.0.3.0 이상) */ + public strikeColor?: ColorRef - fontScale: SupportedLocaleOptions + static fromRecord(record: HWPRecord, version: HWPVersion) { + if (record.id !== DocInfoTagID.HWPTAG_CHAR_SHAPE) { + throw new Error('DocInfo: CharShape: Record has wrong ID') + } - fontSpacing: SupportedLocaleOptions + const reader = new ByteReader(record.data) - fontRatio: SupportedLocaleOptions + const fontIds: SupportedLocaleOptions = [ + reader.readUInt16(), + reader.readUInt16(), + reader.readUInt16(), + reader.readUInt16(), + reader.readUInt16(), + reader.readUInt16(), + reader.readUInt16(), + ] - fontLocation: SupportedLocaleOptions + const fontScales: SupportedLocaleOptions = [ + reader.readUInt8(), + reader.readUInt8(), + reader.readUInt8(), + reader.readUInt8(), + reader.readUInt8(), + reader.readUInt8(), + reader.readUInt8(), + ] - fontBaseSize: number + const fontSpacings: SupportedLocaleOptions = [ + reader.readInt8(), + reader.readInt8(), + reader.readInt8(), + reader.readInt8(), + reader.readInt8(), + reader.readInt8(), + reader.readInt8(), + ] - attr: number + const fontSizes: SupportedLocaleOptions = [ + reader.readUInt8(), + reader.readUInt8(), + reader.readUInt8(), + reader.readUInt8(), + reader.readUInt8(), + reader.readUInt8(), + reader.readUInt8(), + ] - shadow: RGB + const fontPositions: SupportedLocaleOptions = [ + reader.readInt8(), + reader.readInt8(), + reader.readInt8(), + reader.readInt8(), + reader.readInt8(), + reader.readInt8(), + reader.readInt8(), + ] - shadow2: RGB + const baseSize = reader.readInt32() - color: RGB + const attribute = reader.readUInt32() + const italic = getFlag(attribute, 0) + const bold = getFlag(attribute, 1) + const underlineKind = mapUnderlineKind(getBitValue(attribute, 2, 3)) + const underlineShape = mapBorderKind(getBitValue(attribute, 4, 7)) + const outlineKind = mapOutlineKind(getBitValue(attribute, 8, 10)) + const shadowKind = mapShadowKind(getBitValue(attribute, 11, 12)) + const emboss = getFlag(attribute, 13) + const engrave = getFlag(attribute, 14) + const supscript = getFlag(attribute, 15) + const subscript = getFlag(attribute, 16) + const strike = getBitValue(attribute, 18, 20) > 0 + const symMark = mapSymMark(getBitValue(attribute, 21, 24)) + const useFontSpace = getFlag(attribute, 25) + const strikeShape = mapBorderKind(getBitValue(attribute, 26, 29)) + const useKerning = getFlag(attribute, 30) - underLineColor: RGB + const shadowOffsetX = reader.readUInt8() + const shadowOffsetY = reader.readUInt8() - shadeColor: RGB + const color = ColorRef.fromBits(reader.readUInt32()) + const underlineColor = ColorRef.fromBits(reader.readUInt32()) - shadowColor: RGB + const shadeColor = ColorRef.fromBits(reader.readUInt32()) + const shadowColor = ColorRef.fromBits(reader.readUInt32()) - fontBackgroundId: number | null = null + const charShape = new CharShapeStyle( + fontIds, + fontScales, + fontSpacings, + fontSizes, + fontPositions, + baseSize, + italic, + bold, + underlineKind, + underlineShape, + outlineKind, + shadowKind, + emboss, + engrave, + supscript, + subscript, + strike, + symMark, + useFontSpace, + strikeShape, + useKerning, + shadowOffsetX, + shadowOffsetY, + color, + underlineColor, + shadeColor, + shadowColor, + ) - strikeColor: RGB | null = null + if (version.gte(new HWPVersion(5, 0, 2, 1))) { + charShape.borderFillId = reader.readUInt16() + } - constructor( - fontId: SupportedLocaleOptions, - fontScale: SupportedLocaleOptions, - fontSpacing: SupportedLocaleOptions, - fontRatio: SupportedLocaleOptions, - fontLocation: SupportedLocaleOptions, - fontBaseSize: number, - attr: number, - shadow: number, - shadow2: number, - color: number, - underLineColor: number, - shadeColor: number, - shadowColor: number, - ) { - this.fontId = fontId - this.fontScale = fontScale - this.fontSpacing = fontSpacing - this.fontRatio = fontRatio - this.fontLocation = fontLocation - this.fontBaseSize = fontBaseSize / 100 - this.attr = attr - this.shadow = getRGB(shadow) - this.shadow2 = getRGB(shadow2) - this.color = getRGB(color) - this.underLineColor = getRGB(underLineColor) - this.shadeColor = getRGB(shadeColor) - this.shadowColor = getRGB(shadowColor) + if (version.gte(new HWPVersion(5, 0, 3, 0))) { + charShape.strikeColor = ColorRef.fromBits(reader.readUInt32()) + } + + if (!reader.isEOF()) { + throw new Error('DocInfo: CharShape: Reader is not EOF') + } + + return charShape + } +} + +export enum UnderlineKind { + None, + Bottom, + Top, +} + +function mapUnderlineKind(value: number) { + if (value >= UnderlineKind.None && value <= UnderlineKind.Top) { + return value as UnderlineKind + } + throw new Error(`Unknown UnderlineKind: ${value}`) +} + +export enum OutlineKind { + /** 없음 */ + None, + /** 실선 */ + Solid, + /** 점선 */ + Dot, + /** 굵은 실선(두꺼운 선) */ + Tick, + /** 파선(긴 점선) */ + Dash, + /** 일점쇄선 (-.-.-.-.) */ + DashDot, + /** 이점쇄선 (-..-..-..) */ + DashDotDot, +} + +function mapOutlineKind(value: number) { + if (value >= OutlineKind.None && value <= OutlineKind.DashDotDot) { + return value as OutlineKind + } + throw new Error(`Unknown OutlineKind: ${value}`) +} + +export enum CharShadowKind { + /** 없음 */ + None, + /** 비연속 */ + Drop, + /** 연속 */ + Continuous, +} + +function mapShadowKind(value: number) { + if (value >= CharShadowKind.None && value <= CharShadowKind.Continuous) { + return value as CharShadowKind + } + throw new Error(`Unknown CharShadowKind: ${value}`) +} + +export enum SymMark { + /** 없음 */ + None, + /** 검정 동그라미 강조점 */ + DotAbove, + /** 속 빈 동그라미 강조점̊̊̊̊̊̊̊̊̊ */ + RingAbove, + /** ˇ */ + Caron, + /** ̃ */ + Tilde, + /** ・ */ + DotMiddle, + /** : */ + Colon, +} + +function mapSymMark(value: number) { + if (value >= SymMark.None && value <= SymMark.Colon) { + return value as SymMark } + throw new Error(`Unknown SymMark: ${value}`) } diff --git a/packages/parser/src/models/doc-info/compatible-document.ts b/packages/parser/src/models/doc-info/compatible-document.ts new file mode 100644 index 0000000..7240791 --- /dev/null +++ b/packages/parser/src/models/doc-info/compatible-document.ts @@ -0,0 +1,68 @@ +/** + * Copyright Han Lee and other contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ByteReader } from '../../utils/byte-reader.js' +import type { HWPRecord } from '../record.js' +import { DocInfoTagID } from '../../constants/tag-id.js' +import { LayoutCompatibility } from './layout-compatibility.js' + +export class CompatibleDocument { + constructor( + /** 대상 프로그램 */ + public targetProgram: TargetProgram, + /** 레이아웃 호환성 */ + public layoutCompatibility: LayoutCompatibility, + ) {} + + static fromRecords( + record: HWPRecord, + records: Generator, + ) { + if (record.id !== DocInfoTagID.HWPTAG_COMPATIBLE_DOCUMENT) { + throw new Error('DocInfo: CompatibleDocument: Record has wrong ID') + } + + const reader = new ByteReader(record.data) + const targetProgram = mapTargetProgram(reader.readUInt32()) + if (!reader.isEOF()) { + throw new Error('DocInfo: CompatibleDocument: Reader is not EOF') + } + + const next = records.next() + if (next.done) { + throw new Error('Unexpected EOF') + } + + const layoutCompatibility = LayoutCompatibility.fromRecord(next.value) + return new CompatibleDocument(targetProgram, layoutCompatibility) + } +} + +export enum TargetProgram { + /** 한/글 문서(현재 버전) */ + HWP201X, + /** 한/글 2007 호환 문서 */ + HWP200X, + /** MS 워드 호환 문서 */ + MSWord, +} + +function mapTargetProgram(value: number) { + if (value >= TargetProgram.HWP201X && value <= TargetProgram.MSWord) { + return value as TargetProgram + } + throw new Error(`Unknown TargetProgram: ${value}`) +} diff --git a/packages/parser/src/models/doc-info/doc-info.ts b/packages/parser/src/models/doc-info/doc-info.ts index 5cd6f0f..bb69666 100644 --- a/packages/parser/src/models/doc-info/doc-info.ts +++ b/packages/parser/src/models/doc-info/doc-info.ts @@ -14,37 +14,70 @@ * limitations under the License. */ -import { CharShape } from './char-shape.js' -import { FontFace } from './font-face.js' -import { BinData } from './bin-data.js' -import { BorderFill } from './border-fill.js' -import { ParagraphShape } from './paragraph-shape.js' -import { StartingIndex } from './starting-index.js' -import { CaratLocation } from './carat-location.js' -import { LayoutCompatibility } from './layout-compatibility.js' +import { find, type CFB$Container } from 'cfb' +import { inflate } from 'pako' + +import type { HWPHeader } from '../header.js' +import { ByteReader } from '../../utils/byte-reader.js' +import { Properties } from './properties.js' +import type { HWPVersion } from '../version.js' +import { IDMappings } from './id-mappings.js' +import { CompatibleDocument } from './compatible-document.js' +import { DocInfoTagID } from '../../constants/tag-id.js' export class DocInfo { - sectionSize: number = 0 + constructor(public properties: Properties, public idMappings: IDMappings) {} + public compatibleDocument?: CompatibleDocument - charShapes: CharShape[] = [] + static fromCfbContainer( + container: CFB$Container, + header: HWPHeader, + ): DocInfo { + const docInfoEntry = find(container, 'DocInfo') - fontFaces: FontFace[] = [] + if (!docInfoEntry) { + throw new Error('DocInfo not exist') + } - binData: BinData[] = [] + if (!ArrayBuffer.isView(docInfoEntry.content)) { + throw new Error('DocInfo content is not ArrayBuffer') + } - borderFills: BorderFill[] = [] + if (header.flags.compressed) { + const decodedContent: Uint8Array = inflate(docInfoEntry.content, { + windowBits: -15, + }) + return DocInfo.fromBytes(decodedContent, header.version) + } + return DocInfo.fromBytes(docInfoEntry.content, header.version) + } - paragraphShapes: ParagraphShape[] = [] + static fromBytes(bytes: Uint8Array, version: HWPVersion): DocInfo { + const reader = new ByteReader(bytes.buffer) + const records = reader.records() - startingIndex: StartingIndex = new StartingIndex() + const properties = Properties.fromRecord(records.next().value!) + const idMappings = IDMappings.fromRecords(records, version) - caratLocation: CaratLocation = new CaratLocation() + const info = new DocInfo(properties, idMappings) - compatibleDocument: number = 0 + while (true) { + const current = records.next() + if (current.done) { + break + } - layoutCompatibility: LayoutCompatibility = new LayoutCompatibility() + switch (current.value.id) { + // TODO: Implement other records + case DocInfoTagID.HWPTAG_COMPATIBLE_DOCUMENT: + info.compatibleDocument = CompatibleDocument.fromRecords( + current.value, + records, + ) + break + } + } - getCharShape(index: number): CharShape | undefined { - return this.charShapes[index] + return info } } diff --git a/packages/parser/src/models/doc-info/font-face.ts b/packages/parser/src/models/doc-info/font-face.ts index e73dfe9..fa3e20c 100644 --- a/packages/parser/src/models/doc-info/font-face.ts +++ b/packages/parser/src/models/doc-info/font-face.ts @@ -14,33 +14,86 @@ * limitations under the License. */ +import { DocInfoTagID } from '../../constants/tag-id.js' +import { getFlag } from '../../utils/bit-utils.js' +import { ByteReader } from '../../utils/byte-reader.js' +import type { HWPRecord } from '../record.js' import { Panose } from './panose.js' export class FontFace { - name: string = '' + constructor( + /** 글꼴 이름 */ + public name: string, + ) {} - alternative: string = '' + /** 기본 글꼴 이름 */ + public defaultFontName?: string + /** 글꼴유형정보 */ + public panose?: Panose + /** 대체 글꼴 유형 */ + public alternativeKind?: AlternativeKind + /** 대체 글꼴 이름 */ + public alternativeFontName?: string - default: string = '' + static fromRecord(record: HWPRecord) { + if (record.id !== DocInfoTagID.HWPTAG_FACE_NAME) { + throw new Error('DocInfo: Font: Record has wrong ID') + } + const reader = new ByteReader(record.data) + + const properties = reader.readUInt8() + const name = reader.readString() - panose: Panose | null = null + const hasAlternative = getFlag(properties, 7) + const hasPanose = getFlag(properties, 6) + const hasDefaultFont = getFlag(properties, 5) - getFontFamily(): string { - const result = [`${this.name}`] + const font = new FontFace(name) - if (this.alternative) { - result.push(`"${this.alternative}"`) + if (hasAlternative) { + font.alternativeKind = mapAlternativeKind(reader.readUInt8()) + font.alternativeFontName = reader.readString() } - if (this.default) { - result.push(`"${this.default}"`) + if (hasPanose) { + font.panose = new Panose( + reader.readUInt8(), + reader.readUInt8(), + reader.readUInt8(), + reader.readUInt8(), + reader.readUInt8(), + reader.readUInt8(), + reader.readUInt8(), + reader.readUInt8(), + reader.readUInt8(), + reader.readUInt8(), + ) } - if (this.panose) { - const panoseFontFamily = this.panose.getFontFamily() - result.push(panoseFontFamily) + if (hasDefaultFont) { + font.defaultFontName = reader.readString() } - return result.join(',') + if (!reader.isEOF()) { + throw new Error('DocInfo: Font: Reader is not EOF') + } + + return font + } +} + +enum AlternativeKind { + /** 원래 종류를 알 수 없을 때 */ + Unknown, + /** 트루타입 글꼴 */ + TTF, + /** 한/글 전용 글꼴 */ + HFT, +} + +function mapAlternativeKind(value: number) { + if (value >= AlternativeKind.Unknown && value <= AlternativeKind.HFT) { + return value as AlternativeKind } + throw new Error(`Unknown AlternativeKind: ${value}`) } diff --git a/packages/parser/src/models/doc-info/id-mappings.ts b/packages/parser/src/models/doc-info/id-mappings.ts new file mode 100644 index 0000000..4e3c737 --- /dev/null +++ b/packages/parser/src/models/doc-info/id-mappings.ts @@ -0,0 +1,163 @@ +/** + * Copyright Han Lee and other contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ByteReader } from '../../utils/byte-reader.js' +import { readItems } from '../../utils/record.js' +import type { HWPRecord } from '../record.js' +import { DocInfoTagID } from '../../constants/tag-id.js' +import { HWPVersion } from '../version.js' +import { BinData } from './bin-data.js' +import { FontFace } from './font-face.js' +import { BorderFill } from './border-fill.js' +import { CharShapeStyle } from './char-shape.js' +import { TabDefinition } from './tab-definition.js' +import { Numbering } from './numbering.js' +import { Bullet } from './bullet.js' +import { ParagraphShape } from './paragraph-shape.js' +import { Style } from './style.js' +import { MemoShape } from './memo-shape.js' +import { ChangeTracking } from './change-tracking.js' +import { ChangeTrackingAuthor } from './change-tracking-author.js' + +const MEMO_SUPPORTED_VERSION = new HWPVersion(5, 0, 2, 1) +const TRACKING_SUPPORTED_VERSION = new HWPVersion(5, 0, 3, 2) + +export class IDMappings { + constructor( + /** 바이너리 데이터 */ + public binaryData: BinData[], + /** 한글 글꼴 */ + public koreanFonts: FontFace[], + /** 영어 글꼴 */ + public englishFonts: FontFace[], + /** 한자 글꼴 */ + public chineseCharactersFonts: FontFace[], + /** 일어 글꼴 */ + public japaneseFonts: FontFace[], + /** 기타 글꼴 */ + public etcFonts: FontFace[], + /** 기호 글꼴 */ + public symbolFonts: FontFace[], + /** 사용자 글꼴 */ + public userFonts: FontFace[], + /** 테두리/배경 */ + public borderFills: BorderFill[], + /** 글자 모양 */ + public charShapes: CharShapeStyle[], + /** 탭 정의 */ + public tabDefinitions: TabDefinition[], + /** 문단 번호 */ + public numberings: Numbering[], + /** 글머리표 */ + public bullets: Bullet[], + /** 문단 모양 */ + public paragraphShapes: ParagraphShape[], + /** 스타일(문단 스타일) */ + public styles: Style[], + /** 메모 모양 (5.0.2.1 이상) */ + public memoShapes: MemoShape[], + /** 변경추적 (5.0.3.2 이상) */ + public changeTrackings: ChangeTracking[], + /** 변경추적 사용자 (5.0.3.2 이상) */ + public changeTrackingAuthors: ChangeTrackingAuthor[], + ) {} + + static fromRecords(records: Generator, version: HWPVersion) { + const record = records.next().value! + if (record.id !== DocInfoTagID.HWPTAG_ID_MAPPINGS) { + throw new Error('DocInfo: IDMappings: Record has wrong ID') + } + + const reader = new ByteReader(record.data) + + const binaryDataCount = reader.readInt32() + const koreanFontsCount = reader.readInt32() + const englishFontsCount = reader.readInt32() + const chineseCharactersFontsCount = reader.readInt32() + const japaneseFontsCount = reader.readInt32() + const etcFontsCount = reader.readInt32() + const symbolFontsCount = reader.readInt32() + const userFontsCount = reader.readInt32() + + const borderFillsCount = reader.readInt32() + const charShapesCount = reader.readInt32() + const tabDefinitionsCount = reader.readInt32() + const numberingsCount = reader.readInt32() + const bulletsCount = reader.readInt32() + const paragraphShapesCount = reader.readInt32() + const stylesCount = reader.readInt32() + + const memoShapesCount = version.gte(MEMO_SUPPORTED_VERSION) + ? reader.readInt32() + : 0 + const changeTrackingsCount = version.gte(TRACKING_SUPPORTED_VERSION) + ? reader.readInt32() + : 0 + const changeTrackingAuthorsCount = version.gte(TRACKING_SUPPORTED_VERSION) + ? reader.readInt32() + : 0 + + if (!reader.isEOF()) { + throw new Error('DocInfo: IDMappings: Reader is not EOF') + } + + return new IDMappings( + readItems(records, binaryDataCount, version, BinData.fromRecord), + readItems(records, koreanFontsCount, version, FontFace.fromRecord), + readItems(records, englishFontsCount, version, FontFace.fromRecord), + readItems( + records, + chineseCharactersFontsCount, + version, + FontFace.fromRecord, + ), + readItems(records, japaneseFontsCount, version, FontFace.fromRecord), + readItems(records, etcFontsCount, version, FontFace.fromRecord), + readItems(records, symbolFontsCount, version, FontFace.fromRecord), + readItems(records, userFontsCount, version, FontFace.fromRecord), + readItems(records, borderFillsCount, version, BorderFill.fromRecord), + readItems(records, charShapesCount, version, CharShapeStyle.fromRecord), + readItems( + records, + tabDefinitionsCount, + version, + TabDefinition.fromRecord, + ), + readItems(records, numberingsCount, version, Numbering.fromRecord), + readItems(records, bulletsCount, version, Bullet.fromRecord), + readItems( + records, + paragraphShapesCount, + version, + ParagraphShape.fromRecord, + ), + readItems(records, stylesCount, version, Style.fromRecord), + readItems(records, memoShapesCount, version, MemoShape.fromRecord), + readItems( + records, + changeTrackingsCount, + version, + ChangeTracking.fromRecord, + ), + readItems( + records, + changeTrackingAuthorsCount, + version, + ChangeTrackingAuthor.fromRecord, + ), + ) + } +} diff --git a/packages/parser/src/models/doc-info/layout-compatibility.ts b/packages/parser/src/models/doc-info/layout-compatibility.ts index e72786f..4fbd5ce 100644 --- a/packages/parser/src/models/doc-info/layout-compatibility.ts +++ b/packages/parser/src/models/doc-info/layout-compatibility.ts @@ -14,14 +14,42 @@ * limitations under the License. */ +import { DocInfoTagID } from '../../constants/tag-id.js' +import { ByteReader } from '../../utils/byte-reader.js' +import type { HWPRecord } from '../record.js' + export class LayoutCompatibility { - char: number = 0 + constructor( + /** 글자 단위 서식 */ + public char: number, + /** 문단 단위 서식 */ + public paragraph: number, + /** 구역 단위 서식 */ + public section: number, + /** 개체 단위 서식 */ + public object: number, + /** 필드 단위 서식 */ + public field: number, + ) {} + + static fromRecord(record: HWPRecord) { + if (record.id !== DocInfoTagID.HWPTAG_LAYOUT_COMPATIBILITY) { + throw new Error('DocInfo: LayoutCompatibility: Record has wrong ID') + } - paragraph: number = 0 + const reader = new ByteReader(record.data) - section: number = 0 + // NOTE: (@hahnlee) 문서와 되어있지 않음, 정확한 정보는 HWPX와 대조해서 유추해야함 + const char = reader.readUInt32() + const paragraph = reader.readUInt32() + const section = reader.readUInt32() + const object = reader.readUInt32() + const field = reader.readUInt32() - object: number = 0 + if (!reader.isEOF()) { + throw new Error('DocInfo: LayoutCompatibility: Reader is not EOF') + } - field: number = 0 + return new LayoutCompatibility(char, paragraph, section, object, field) + } } diff --git a/packages/parser/src/models/doc-info/memo-shape.ts b/packages/parser/src/models/doc-info/memo-shape.ts new file mode 100644 index 0000000..438f8d7 --- /dev/null +++ b/packages/parser/src/models/doc-info/memo-shape.ts @@ -0,0 +1,29 @@ +/** + * Copyright Han Lee and other contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DocInfoTagID } from '../../constants/tag-id.js' +import type { HWPRecord } from '../record.js' + +export class MemoShape { + static fromRecord(record: HWPRecord) { + if (record.id !== DocInfoTagID.HWPTAG_MEMO_SHAPE) { + throw new Error('DocInfo: MemoShape: Record has wrong ID') + } + + // TODO: implement + return new MemoShape() + } +} diff --git a/packages/parser/src/models/doc-info/numbering.ts b/packages/parser/src/models/doc-info/numbering.ts index fce31b0..5270045 100644 --- a/packages/parser/src/models/doc-info/numbering.ts +++ b/packages/parser/src/models/doc-info/numbering.ts @@ -1,3 +1,19 @@ +/** + * Copyright Han Lee and other contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { DocInfoTagID } from '../../constants/tag-id.js' import { getBitValue, getFlag } from '../../utils/bit-utils.js' import { ByteReader } from '../../utils/byte-reader.js' diff --git a/packages/parser/src/models/doc-info/panose.ts b/packages/parser/src/models/doc-info/panose.ts index 9ad70b1..05dfe08 100644 --- a/packages/parser/src/models/doc-info/panose.ts +++ b/packages/parser/src/models/doc-info/panose.ts @@ -15,45 +15,30 @@ */ /** - * Panose 1.0 - * @see https://www.w3.org/Printing/stevahn.html + * @link https://en.wikipedia.org/wiki/PANOSE + * @link https://monotype.github.io/panose/pan1.htm */ export class Panose { - family: number = 0 - - serifStyle: number = 0 - - weight: number = 0 - - proportion: number = 0 - - contrast: number = 0 - - strokeVariation: number = 0 - - armStyle: number = 0 - - letterForm: number = 0 - - midline: number = 0 - - xHeight: number = 0 - - getFontFamily() { - if (this.family === 3) { - return 'cursive' - } - - if (this.family === 2) { - if (this.serifStyle > 1 && this.serifStyle < 11) { - return 'sans' - } - - if (this.serifStyle > 10 && this.serifStyle < 14) { - return 'sans-serf' - } - } - - return '' - } + constructor( + /** 글꼴 계열 */ + public kind: number, + /** 세리프 유형 */ + public serifStyle: number, + /** 굵기 */ + public weight: number, + /** 비례 */ + public proportion: number, + /** 대조 */ + public contrast: number, + /** 스트로크 편차 */ + public strokeVariation: number, + /** 자획 유형 */ + public armStyle: number, + /** 글자형 */ + public letterform: number, + /** 중간선 */ + public midline: number, + /** X-높이 */ + public xHeight: number, + ) {} } diff --git a/packages/parser/src/models/doc-info/paragraph-shape.ts b/packages/parser/src/models/doc-info/paragraph-shape.ts index abb29a5..c0fc74b 100644 --- a/packages/parser/src/models/doc-info/paragraph-shape.ts +++ b/packages/parser/src/models/doc-info/paragraph-shape.ts @@ -14,6 +14,301 @@ * limitations under the License. */ +import { DocInfoTagID } from '../../constants/tag-id.js' +import { getBitValue, getFlag } from '../../utils/bit-utils.js' +import { ByteReader } from '../../utils/byte-reader.js' +import type { HWPRecord } from '../record.js' +import { HWPVersion } from '../version.js' + export class ParagraphShape { - align: number = 0 + constructor( + /** 줄 간격 종류. 한/글 2007 이하 버전에서 사용. */ + public lineSpaceKindOld: LineSpacingKind, + /** 정렬 방식 */ + public align: TextAlign, + /** 라틴 문자의 줄나눔 단위 */ + public breakLatinWord: BreakLatinWord, + /** 라틴 문자 이외의 줄나눔 단위 */ + public breakNonLatinWord: BreakNonLatinWord, + /** 편집 용지의 줄 격자 사용 여부 */ + public snapToGrid: boolean, + /** 공백 최소값 */ + public condense: number, + /** 외톨이줄 보호 여부 */ + public widowOrphan: boolean, + /** 다음 문단과 함께 여부 */ + public keepWithNext: boolean, + /** 문단 보호 여부 */ + public keepLines: boolean, + /** 문단 앞에서 항상 쪽 나눔 여부 */ + public pageBreakBefore: boolean, + /** 세로 정렬 */ + public verticalAlign: VerticalAlign, + /** 글꼴에 어울리는 줄 높이 여부 */ + public fontLineHeight: boolean, + /** 문단 머리 모양 종류 */ + public headingKind: ParagraphHeadingKind, + /** 문단 수준 */ + public headingLevel: number, + /** 문단 테두리 연결 여부 */ + public borderConnect: boolean, + /** 문단 여백 무시 여부 */ + public borderIgnoreMargin: boolean, + /** 문단 꼬리 모양 */ + public tailing: number, + /** 왼쪽 여백 */ + public paddingLeft: number, + /** 오른쪽 여백 */ + public paddingRight: number, + /** 들여 쓰기/내어 쓰기 */ + public indent: number, + /** 문단 간격 위 */ + public marginTop: number, + /** 문단 간격 아래 */ + public marginBottom: number, + /** 줄 간격. 한글 2007 이하 버전(5.0.2.5 버전 미만)에서 사용. */ + public lineSpaceOld: number, + /** 탭 정의 아이디(TabDef ID) 참조 값 */ + public tabDefinitionId: number, + /** 번호 문단 ID(Numbering ID) 또는 글머리표 문단 모양 ID(Bullet ID) 참조 값 */ + public numberingBulletId: number, + /** 테두리/배경 모양 ID(BorderFill ID) 참조 값 */ + public borderFillId: number, + /** 문단 테두리 왼쪽 간격 */ + public borderOffsetLeft: number, + /** 문단 테두리 오른쪽 간격 */ + public borderOffsetRight: number, + /** 문단 테두리 위쪽 간격 */ + public borderOffsetTop: number, + /** 문단 테두리 아래쪽 간격 */ + public borderOffsetBottom: number, + ) {} + /** 한 줄로 입력 여부 (5.0.1.7 버전 이상) */ + public singleLine?: boolean + /** 한글과 영어 간격을 자동 조절 여부 (5.0.1.7 버전 이상) */ + public autoSpacingKrEng?: boolean + /** 한글과 숫자 간격을 자동 조절 여부 (5.0.1.7 버전 이상) */ + public autoSpacingKrNum?: boolean + /** 줄 간격 종류 (5.0.2.5 버전 이상) */ + public lineSpacingKind?: LineSpacingKind + /** 줄 간격 (5.0.2.5 버전 이상) */ + public lineSpacing?: number + /** 개요 수준 (5.1.0.0 버전 이상) */ + public paraLevel?: number + + static fromRecord(record: HWPRecord, version: HWPVersion) { + if (record.id !== DocInfoTagID.HWPTAG_PARA_SHAPE) { + throw new Error('DocInfo: ParagraphShape: Record has wrong ID') + } + + const reader = new ByteReader(record.data) + + const attribute = reader.readUInt32() + const lineSpaceKindOld = mapLineSpacingKind(getBitValue(attribute, 0, 1)) + const align = mapAlign(getBitValue(attribute, 2, 4)) + const breakLatinWord = mapBreakLatinWord(getBitValue(attribute, 5, 6)) + const breakNonLatinWord = mapBreakNonLatinWord(getBitValue(attribute, 7, 7)) + const snapToGrid = getFlag(attribute, 8) + const condense = getBitValue(attribute, 9, 15) + const widowOrphan = getFlag(attribute, 16) + const keepWithNext = getFlag(attribute, 17) + const keepLines = getFlag(attribute, 18) + const pageBreakBefore = getFlag(attribute, 19) + const verticalAlign = mapVerticalAlign(getBitValue(attribute, 20, 21)) + const fontLineHeight = getFlag(attribute, 22) + const headingKind = mapParagraphHeadingKind(getBitValue(attribute, 23, 24)) + const headingLevel = getBitValue(attribute, 25, 27) + const borderConnect = getFlag(attribute, 28) + const borderIgnoreMargin = getFlag(attribute, 29) + const tailing = getBitValue(attribute, 30, 30) + + const paddingLeft = reader.readInt32() + const paddingRight = reader.readInt32() + + const indent = reader.readInt32() + + const marginTop = reader.readInt32() + const marginBottom = reader.readInt32() + + const lineSpaceOld = reader.readInt32() + + const tabDefinitionId = reader.readUInt16() + const numberingBulletId = reader.readUInt16() + const borderFillId = reader.readUInt16() + + const borderOffsetLeft = reader.readInt16() + const borderOffsetRight = reader.readInt16() + const borderOffsetTop = reader.readInt16() + const borderOffsetBottom = reader.readInt16() + + const paragraphShape = new ParagraphShape( + lineSpaceKindOld, + align, + breakLatinWord, + breakNonLatinWord, + snapToGrid, + condense, + widowOrphan, + keepWithNext, + keepLines, + pageBreakBefore, + verticalAlign, + fontLineHeight, + headingKind, + headingLevel, + borderConnect, + borderIgnoreMargin, + tailing, + paddingLeft, + paddingRight, + indent, + marginTop, + marginBottom, + lineSpaceOld, + tabDefinitionId, + numberingBulletId, + borderFillId, + borderOffsetLeft, + borderOffsetRight, + borderOffsetTop, + borderOffsetBottom, + ) + + if (version.gte(new HWPVersion(5, 0, 1, 7))) { + const attributeV2 = reader.readUInt32() + paragraphShape.singleLine = getBitValue(attributeV2, 0, 1) > 0 + paragraphShape.autoSpacingKrEng = getFlag(attributeV2, 4) + paragraphShape.autoSpacingKrNum = getFlag(attributeV2, 5) + } + + if (version.gte(new HWPVersion(5, 0, 2, 5))) { + const attributeV3 = reader.readUInt32() + paragraphShape.lineSpacingKind = mapLineSpacingKind( + getBitValue(attributeV3, 0, 4), + ) + paragraphShape.lineSpacing = reader.readUInt32() + } + + if (version.gte(new HWPVersion(5, 1, 0, 0))) { + paragraphShape.paraLevel = reader.readUInt32() + } + + if (!reader.isEOF()) { + throw new Error('DocInfo: ParagraphShape: Reader is not EOF') + } + + return paragraphShape + } +} + +export enum TextAlign { + /** 양쪽 정렬 */ + Justify, + /** 왼쪽 정렬 */ + Left, + /** 오른쪽 정렬 */ + Right, + /** 가운데 정렬 */ + Center, + /** 배분 정렬 */ + Distributive, + /** 나눔 정렬 */ + DistributiveSpace, +} + +function mapAlign(value: number) { + if (value >= TextAlign.Justify && value <= TextAlign.DistributiveSpace) { + return value as TextAlign + } + throw new Error(`Unknown Align: ${value}`) +} + +export enum BreakLatinWord { + /** 단어 */ + KeepWord, + /** 하이픈 */ + Hyphenation, + /** 글자 */ + BreakWord, +} + +function mapBreakLatinWord(value: number) { + if (value >= BreakLatinWord.KeepWord && value <= BreakLatinWord.BreakWord) { + return value as BreakLatinWord + } + throw new Error(`Unknown BreakLatinWord: ${value}`) +} + +export enum BreakNonLatinWord { + /** 단어 */ + KeepWord, + /** 글자 */ + BreakWord, +} + +function mapBreakNonLatinWord(value: number) { + if ( + value >= BreakNonLatinWord.KeepWord && + value <= BreakNonLatinWord.BreakWord + ) { + return value as BreakNonLatinWord + } + throw new Error(`Unknown BreakNonLatinWord: ${value}`) +} + +export enum VerticalAlign { + /** 글꼴기준 */ + Baseline, + /** 위쪽 */ + Top, + /** 가운데 */ + Center, + /** 아래 */ + Bottom, +} + +function mapVerticalAlign(value: number) { + if (value >= VerticalAlign.Baseline && value <= VerticalAlign.Bottom) { + return value as VerticalAlign + } + throw new Error(`Unknown VerticalAlign: ${value}`) +} + +export enum ParagraphHeadingKind { + /** 없음 */ + None, + /** 개요 */ + Outline, + /** 번호 */ + Number, + /** 글머리표 */ + Bullet, +} + +function mapParagraphHeadingKind(value: number) { + if ( + value >= ParagraphHeadingKind.None && + value <= ParagraphHeadingKind.Bullet + ) { + return value as ParagraphHeadingKind + } + throw new Error(`Unknown ParagraphHeadingKind: ${value}`) +} + +export enum LineSpacingKind { + /** 글자에 따라 (%) */ + Percent, + /** 고정값 */ + Fixed, + /** 여백만 지정 */ + BetweenLine, + /** 최소 (5.0.2.5 버전 이상) */ + AtLeast, +} + +function mapLineSpacingKind(value: number) { + if (value >= LineSpacingKind.Percent && value <= LineSpacingKind.AtLeast) { + return value as LineSpacingKind + } + throw new Error(`Unknown LineSpacingKind: ${value}`) } diff --git a/packages/parser/src/models/doc-info/properties.ts b/packages/parser/src/models/doc-info/properties.ts new file mode 100644 index 0000000..cf2f174 --- /dev/null +++ b/packages/parser/src/models/doc-info/properties.ts @@ -0,0 +1,55 @@ +/** + * Copyright Han Lee and other contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ByteReader } from '../../utils/byte-reader.js' +import { HWPRecord } from '../record.js' + +export class Properties { + constructor( + public sections: number, + public pageStartNumber: number, + public footnoteStartNumber: number, + public endnoteStartNumber: number, + public pictureStartNumber: number, + public tableStartNumber: number, + public formulaStartNumber: number, + public listId: number, + public paragraphId: number, + public characterInParagraph: number, + ) {} + + static fromRecord(record: HWPRecord) { + const reader = new ByteReader(record.data) + const properties = new Properties( + reader.readUInt16(), + reader.readUInt16(), + reader.readUInt16(), + reader.readUInt16(), + reader.readUInt16(), + reader.readUInt16(), + reader.readUInt16(), + reader.readUInt32(), + reader.readUInt32(), + reader.readUInt32(), + ) + + if (!reader.isEOF()) { + throw new Error('DocInfo: Properties: Reader is not EOF') + } + + return properties + } +} diff --git a/packages/parser/src/models/doc-info/style.ts b/packages/parser/src/models/doc-info/style.ts new file mode 100644 index 0000000..24d8899 --- /dev/null +++ b/packages/parser/src/models/doc-info/style.ts @@ -0,0 +1,97 @@ +/** + * Copyright Han Lee and other contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DocInfoTagID } from '../../constants/tag-id.js' +import { ByteReader } from '../../utils/byte-reader.js' +import type { HWPRecord } from '../record.js' + +export class Style { + constructor( + /** 로컬 스타일 이름. 한글 윈도우에서는 한글 스타일 이름 */ + public name: string, + /** 영문 스타일 이름 */ + public englishName: string, + /** 스타일 종류 */ + public kind: StyleKind, + /** + * 다음 스타일 아이디 참조값 + * 문단 스타일에서 사용자가 리턴키를 입력하여 다음 문단으로 이동했을때 적용할 스타일 + */ + public nextStyleId: number, + /** + * 언어코드 + * @link https://learn.microsoft.com/en-us/openspecs/office_standards/ms-oe376/6c085406-a698-4e12-9d4d-c3b0ee3dbc4a + */ + public langId: number, + /** 문단 모양 아이디 참조값(문단 모양의 아이디 속성) */ + public paragraphShapeId: number, + /** 글자 모양 아이디(글자 모양의 아이디 속성) */ + public charShapeId: number, + /** + * HWP 포맷문서에는 없지만 HWPX 포맷문서에 정의되어 있음 + * 양식모드에서 style 보호하기 여부 + */ + public lockForm: number, + ) {} + + static fromRecord(record: HWPRecord) { + if (record.id !== DocInfoTagID.HWPTAG_STYLE) { + throw new Error('DocInfo: Style: Record has wrong ID') + } + + const reader = new ByteReader(record.data) + + const name = reader.readString() + const englishName = reader.readString() + const kind = mapStyleKind(reader.readUInt8()) + const nextStyleId = reader.readUInt8() + const langId = reader.readUInt16() + const paragraphShapeId = reader.readUInt16() + const charShapeId = reader.readUInt16() + + // NOTE: (@hahnlee) HWP 포맷문서에는 없지만 HWPX 포맷문서에 정의되어 있음 + const lockForm = reader.readUInt16() + + if (!reader.isEOF()) { + throw new Error('DocInfo: Style: Reader is not EOF') + } + + return new Style( + name, + englishName, + kind, + nextStyleId, + langId, + paragraphShapeId, + charShapeId, + lockForm, + ) + } +} + +export enum StyleKind { + /** 문단 스타일 */ + Para, + /** 글자 스타일 */ + Char, +} + +function mapStyleKind(value: number) { + if (value >= StyleKind.Para && value <= StyleKind.Char) { + return value as StyleKind + } + throw new Error(`Unknown StyleKind: ${value}`) +} diff --git a/packages/parser/src/models/doc-info/tab-definition.ts b/packages/parser/src/models/doc-info/tab-definition.ts new file mode 100644 index 0000000..6c3858c --- /dev/null +++ b/packages/parser/src/models/doc-info/tab-definition.ts @@ -0,0 +1,77 @@ +/** + * Copyright Han Lee and other contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DocInfoTagID } from '../../constants/tag-id.js' +import { getFlag } from '../../utils/bit-utils.js' +import { ByteReader } from '../../utils/byte-reader.js' +import type { HWPRecord } from '../record.js' + +export class TabDefinition { + constructor( + public leftTab: boolean, + public rightTab: boolean, + public tabInfos: TabInfo[], + ) {} + + static fromRecord(record: HWPRecord) { + if (record.id !== DocInfoTagID.HWPTAG_TAB_DEF) { + throw new Error('DocInfo: TabDefinition: Record has wrong ID') + } + + const reader = new ByteReader(record.data) + + const attribute = reader.readUInt32() + const leftTab = getFlag(attribute, 0) + const rightTab = getFlag(attribute, 1) + + const count = reader.readUInt32() + const tabInfos: TabInfo[] = [] + for (let i = 0; i < count; i++) { + tabInfos.push(TabInfo.fromReader(reader)) + } + + if (!reader.isEOF()) { + throw new Error('DocInfo: TabDefinition: Reader is not EOF') + } + + return new TabDefinition(leftTab, rightTab, tabInfos) + } +} + +export class TabInfo { + constructor( + public position: number, + public kind: TabKind, + public borderKind: number, + ) {} + + static fromReader(reader: ByteReader) { + const position = reader.readUInt32() + const kind = reader.readUInt8() + const borderKind = reader.readUInt8() + // 8 바이트를 맞추기 위한 예약 + reader.readUInt16() + + return new TabInfo(position, kind, borderKind) + } +} + +export enum TabKind { + Left, + Right, + Center, + Decimal, +} diff --git a/packages/parser/src/parse.ts b/packages/parser/src/parse.ts index 56c2940..501328e 100644 --- a/packages/parser/src/parse.ts +++ b/packages/parser/src/parse.ts @@ -20,30 +20,11 @@ import { type CFB$Blob, type CFB$Container, } from 'cfb' -import { inflate } from 'pako' import { HWPDocument } from './models/document.js' import { DocInfo } from './models/doc-info/doc-info.js' import { HWPHeader } from './models/header.js' import { Section } from './models/section.js' -import { DocInfoParser } from './doc-info-parser.js' - -function parseDocInfo(container: CFB$Container, header: HWPHeader): DocInfo { - const docInfoEntry = find(container, 'DocInfo') - - if (!docInfoEntry) { - throw new Error('DocInfo not exist') - } - - const content = docInfoEntry.content - - if (header.flags.compressed) { - const decodedContent = inflate(Uint8Array.from(content), { windowBits: -15 }) - return new DocInfoParser(header, decodedContent, container).parse() - } else { - return new DocInfoParser(header, Uint8Array.from(content), container).parse() - } -} function parseSection(container: CFB$Container, header: HWPHeader, sectionNumber: number): Section { const entry = find(container, `Root Entry/BodyText/Section${sectionNumber}`) @@ -61,11 +42,11 @@ export function parse(input: CFB$Blob): HWPDocument { }) const header = HWPHeader.fromCfbContainer(container) - const docInfo = parseDocInfo(container, header) + const docInfo = DocInfo.fromCfbContainer(container, header) const sections: Section[] = [] - for (let i = 0; i < docInfo.sectionSize; i += 1) { + for (let i = 0; i < docInfo.properties.sections; i += 1) { sections.push(parseSection(container, header, i)) } diff --git a/packages/parser/src/utils/record.ts b/packages/parser/src/utils/record.ts index 832733b..5cdf00b 100644 --- a/packages/parser/src/utils/record.ts +++ b/packages/parser/src/utils/record.ts @@ -15,8 +15,28 @@ */ import type { HWPRecord } from '../models/record.js' +import type { HWPVersion } from '../models/version.js' import type { PeekableIterator } from './generator.js' +type FromRecord = (record: HWPRecord, version: HWPVersion) => T + +export function readItems>( + records: Generator, + count: number, + version: HWPVersion, + fromRecord: T, +) { + const items: ReturnType[] = [] + for (let i = 0; i < count; i++) { + const record = records.next() + if (record.done) { + throw new Error('Unexpected EOF') + } + items.push(fromRecord(record.value, version)) + } + return items +} + export function collectChildren( iterator: PeekableIterator, level: number,