diff --git a/.swiftlint.yml b/.swiftlint.yml index be49a56..f585148 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,6 +1,7 @@ excluded: - Carthage - Pods + - .build line_length: 100 diff --git a/README.md b/README.md index 236e155..d30ddf5 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@ ![Lint](https://github.com/sboh1214/HwpKit/workflows/Lint/badge.svg) [![codecov](https://codecov.io/gh/sboh1214/HwpKit/branch/master/graph/badge.svg)](https://codecov.io/gh/sboh1214/HwpKit) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=sboh1214_HwpKit&metric=alert_status)](https://sonarcloud.io/dashboard?id=sboh1214_HwpKit) +[![CodeFactor](https://www.codefactor.io/repository/github/sboh1214/hwpkit/badge)](https://www.codefactor.io/repository/github/sboh1214/hwpkit) + Swift Package for Reading & Writing HWP File ## Install diff --git a/Sources/HwpKit/Enums/HwpCharType.swift b/Sources/HwpKit/Enums/HwpCharType.swift new file mode 100644 index 0000000..aaa31d3 --- /dev/null +++ b/Sources/HwpKit/Enums/HwpCharType.swift @@ -0,0 +1,7 @@ +import Foundation + +public enum HwpCharType: String, Codable { + case char + case inline + case extended +} diff --git a/Sources/HwpKit/Enums/HwpStreamName.swift b/Sources/HwpKit/Enums/HwpStreamName.swift index eb0361d..b4d7c6e 100644 --- a/Sources/HwpKit/Enums/HwpStreamName.swift +++ b/Sources/HwpKit/Enums/HwpStreamName.swift @@ -1,6 +1,7 @@ public enum HwpStreamName: String { case fileHeader = "FileHeader" case docInfo = "DocInfo" + case bodyText = "BodyText" case summary = "\005HwpSummaryInformation" case previewText = "PrvText" case previewImage = "PrvImage" diff --git a/Sources/HwpKit/Enums/HwpTag.swift b/Sources/HwpKit/Enums/HwpTag.swift index f49b416..7149831 100644 --- a/Sources/HwpKit/Enums/HwpTag.swift +++ b/Sources/HwpKit/Enums/HwpTag.swift @@ -1,67 +1,65 @@ -// swiftlint:disable identifier_name - let BEGIN: UInt32 = 0x10 /** ’문서 정보’의 데이터 레코드 */ final class HwpDocInfoTag { - static let DOCUMENT_PROPERTIES: UInt32 = BEGIN - static let ID_MAPPINGS: UInt32 = BEGIN + 1 - static let BIN_DATA: UInt32 = BEGIN + 2 - static let FACE_NAME: UInt32 = BEGIN + 3 - static let BORDER_FILL: UInt32 = BEGIN + 4 - static let CHAR_SHAPE: UInt32 = BEGIN + 5 - static let TAB_DEF: UInt32 = BEGIN + 6 - static let NUMBERING: UInt32 = BEGIN + 7 - static let BULLET: UInt32 = BEGIN + 8 - static let PARA_SHAPE: UInt32 = BEGIN + 9 - static let STYLE: UInt32 = BEGIN + 10 - static let DOC_DATA: UInt32 = BEGIN + 11 - static let DISTRIBUTE_DOC_DATA: UInt32 = BEGIN + 12 - static let RESERVED: UInt32 = BEGIN + 13 - static let COMPATIBLE_DOCUMENT: UInt32 = BEGIN + 14 - static let LAYOUT_COMPATIBILITY: UInt32 = BEGIN + 15 - static let TRACKCHANGE: UInt32 = BEGIN + 16 - static let MEMO_SHAPE: UInt32 = BEGIN + 76 - static let FORBIDDEN_CHAR: UInt32 = BEGIN + 78 - static let TRACK_CHANGE: UInt32 = BEGIN + 80 - static let TRACK_CHANGE_AUTHOR: UInt32 = BEGIN + 81 + static let documentProperties: UInt32 = BEGIN + static let idMappings: UInt32 = BEGIN + 1 + static let binData: UInt32 = BEGIN + 2 + static let faceName: UInt32 = BEGIN + 3 + static let borderFill: UInt32 = BEGIN + 4 + static let charShape: UInt32 = BEGIN + 5 + static let tabDef: UInt32 = BEGIN + 6 + static let numbering: UInt32 = BEGIN + 7 + static let bullet: UInt32 = BEGIN + 8 + static let paraShape: UInt32 = BEGIN + 9 + static let style: UInt32 = BEGIN + 10 + static let docData: UInt32 = BEGIN + 11 + static let distributeDocData: UInt32 = BEGIN + 12 + static let reserved: UInt32 = BEGIN + 13 + static let compatibleDocument: UInt32 = BEGIN + 14 + static let layoutCompatibility: UInt32 = BEGIN + 15 + static let trackChange: UInt32 = BEGIN + 16 + static let memoShape: UInt32 = BEGIN + 76 + static let forbiddenChar: UInt32 = BEGIN + 78 + static let trackChangeContent: UInt32 = BEGIN + 80 + static let trackChangeAuthor: UInt32 = BEGIN + 81 } /** ‘본문’의 데이터 레코드 */ final class HwpSectionTag { - static let PARA_HEADER: UInt32 = BEGIN + 50 - static let PARA_TEXT: UInt32 = BEGIN + 51 - static let PARA_CHAR_SHAPE: UInt32 = BEGIN + 52 - static let PARA_LINE_SEG: UInt32 = BEGIN + 53 - static let PARA_RANGE_TAG: UInt32 = BEGIN + 54 - static let CTRL_HEADER: UInt32 = BEGIN + 55 - static let LIST_HEADER: UInt32 = BEGIN + 56 - static let PAGE_DEF: UInt32 = BEGIN + 57 - static let FOOTNOTE_SHAPE: UInt32 = BEGIN + 58 - static let PAGE_BORDER_FILL: UInt32 = BEGIN + 59 - static let SHAPE_COMPONENT: UInt32 = BEGIN + 60 - static let TABLE: UInt32 = BEGIN + 61 - static let SHAPE_COMPONENT_LINE: UInt32 = BEGIN + 62 - static let SHAPE_COMPONENT_RECTANGLE: UInt32 = BEGIN + 63 - static let SHAPE_COMPONENT_ELLIPSE: UInt32 = BEGIN + 64 - static let SHAPE_COMPONENT_ARC: UInt32 = BEGIN + 65 - static let SHAPE_COMPONENT_POLYGON: UInt32 = BEGIN + 66 - static let SHAPE_COMPONENT_CURVE: UInt32 = BEGIN + 67 - static let SHAPE_COMPONENT_OLE: UInt32 = BEGIN + 68 - static let SHAPE_COMPONENT_PICTURE: UInt32 = BEGIN + 69 - static let SHAPE_COMPONENT_CONTAINER: UInt32 = BEGIN + 70 - static let CTRL_DATA: UInt32 = BEGIN + 71 - static let EQEDIT: UInt32 = BEGIN + 72 - static let RESERVED: UInt32 = BEGIN + 73 - static let SHAPE_COMPONENT_TEXTART: UInt32 = BEGIN + 74 - static let FORM_OBJECT: UInt32 = BEGIN + 75 - static let MEMO_SHAPE: UInt32 = BEGIN + 76 - static let MEMO_LIST: UInt32 = BEGIN + 77 - static let CHART_DATA: UInt32 = BEGIN + 79 - static let VIDEO_DATA: UInt32 = BEGIN + 82 - static let SHAPE_COMPONENT_UNKNOWN: UInt32 = BEGIN + 99 + static let paraHeader: UInt32 = BEGIN + 50 + static let paraText: UInt32 = BEGIN + 51 + static let paraCharShape: UInt32 = BEGIN + 52 + static let paraLineSeg: UInt32 = BEGIN + 53 + static let paraRangeTag: UInt32 = BEGIN + 54 + static let ctrlHeader: UInt32 = BEGIN + 55 + static let listHeader: UInt32 = BEGIN + 56 + static let pageDef: UInt32 = BEGIN + 57 + static let footnoteShape: UInt32 = BEGIN + 58 + static let pageBorderFill: UInt32 = BEGIN + 59 + static let shapeComponent: UInt32 = BEGIN + 60 + static let table: UInt32 = BEGIN + 61 + static let shapeComponentLine: UInt32 = BEGIN + 62 + static let shapeComponentRectangle: UInt32 = BEGIN + 63 + static let shapeComponentEllipse: UInt32 = BEGIN + 64 + static let shapeComponentArc: UInt32 = BEGIN + 65 + static let shapeComponentPolygon: UInt32 = BEGIN + 66 + static let shapeComponentCurve: UInt32 = BEGIN + 67 + static let shapeComponentOle: UInt32 = BEGIN + 68 + static let shapeComponentPicture: UInt32 = BEGIN + 69 + static let shapeComponentContainer: UInt32 = BEGIN + 70 + static let ctrlData: UInt32 = BEGIN + 71 + static let eqEdit: UInt32 = BEGIN + 72 + static let reserved: UInt32 = BEGIN + 73 + static let shapeComponentTextart: UInt32 = BEGIN + 74 + static let formObject: UInt32 = BEGIN + 75 + static let memoShape: UInt32 = BEGIN + 76 + static let memoList: UInt32 = BEGIN + 77 + static let chartData: UInt32 = BEGIN + 79 + static let videoData: UInt32 = BEGIN + 82 + static let shapeComponentUnknown: UInt32 = BEGIN + 99 } diff --git a/Sources/HwpKit/HwpFile.swift b/Sources/HwpKit/HwpFile.swift index dcc88a2..6c0fa8a 100644 --- a/Sources/HwpKit/HwpFile.swift +++ b/Sources/HwpKit/HwpFile.swift @@ -5,6 +5,7 @@ public class HwpFile { public let fileHeader: HwpFileHeader public let docInfo: HwpDocInfo public let previewText: HwpPreviewText + public let sectionArray: [HwpSection] public init(filePath: String) throws { let ole: OLEFile @@ -16,11 +17,15 @@ public class HwpFile { let streams = Dictionary(uniqueKeysWithValues: ole.root.children.map { ($0.name, $0 ) }) let reader = StreamReader(ole, streams) - fileHeader = try HwpFileHeader(reader.getDataFromStream(.fileHeader, false)) + let fileHeader = try HwpFileHeader(reader.getDataFromStream(.fileHeader, false)) + self.fileHeader = fileHeader let docInfoData = try reader.getDataFromStream(.docInfo, fileHeader.isCompressed) docInfo = try HwpDocInfo(docInfoData, fileHeader.version) + sectionArray = try reader.getDataFromStorage(.bodyText, fileHeader.isCompressed) + .map {try HwpSection($0, fileHeader.version)} + guard let previewTextStream = streams[HwpStreamName.previewText.rawValue] else { throw HwpError.streamDoesNotExist(name: HwpStreamName.previewText) } diff --git a/Sources/HwpKit/Models/Document Properties/HwpCaratLocation.swift b/Sources/HwpKit/Model/DocInfo/Document Properties/HwpCaratLocation.swift similarity index 100% rename from Sources/HwpKit/Models/Document Properties/HwpCaratLocation.swift rename to Sources/HwpKit/Model/DocInfo/Document Properties/HwpCaratLocation.swift diff --git a/Sources/HwpKit/Models/Document Properties/HwpDocumentProperties.swift b/Sources/HwpKit/Model/DocInfo/Document Properties/HwpDocumentProperties.swift similarity index 100% rename from Sources/HwpKit/Models/Document Properties/HwpDocumentProperties.swift rename to Sources/HwpKit/Model/DocInfo/Document Properties/HwpDocumentProperties.swift diff --git a/Sources/HwpKit/Models/Document Properties/HwpStartingIndex.swift b/Sources/HwpKit/Model/DocInfo/Document Properties/HwpStartingIndex.swift similarity index 100% rename from Sources/HwpKit/Models/Document Properties/HwpStartingIndex.swift rename to Sources/HwpKit/Model/DocInfo/Document Properties/HwpStartingIndex.swift diff --git a/Sources/HwpKit/Models/HwpBinData.swift b/Sources/HwpKit/Model/DocInfo/HwpBinData.swift similarity index 100% rename from Sources/HwpKit/Models/HwpBinData.swift rename to Sources/HwpKit/Model/DocInfo/HwpBinData.swift diff --git a/Sources/HwpKit/Models/HwpBorderFill.swift b/Sources/HwpKit/Model/DocInfo/HwpBorderFill.swift similarity index 100% rename from Sources/HwpKit/Models/HwpBorderFill.swift rename to Sources/HwpKit/Model/DocInfo/HwpBorderFill.swift diff --git a/Sources/HwpKit/Models/HwpCharShape.swift b/Sources/HwpKit/Model/DocInfo/HwpCharShape.swift similarity index 100% rename from Sources/HwpKit/Models/HwpCharShape.swift rename to Sources/HwpKit/Model/DocInfo/HwpCharShape.swift diff --git a/Sources/HwpKit/Models/HwpFaceName.swift b/Sources/HwpKit/Model/DocInfo/HwpFaceName.swift similarity index 100% rename from Sources/HwpKit/Models/HwpFaceName.swift rename to Sources/HwpKit/Model/DocInfo/HwpFaceName.swift diff --git a/Sources/HwpKit/Models/HwpParaShape.swift b/Sources/HwpKit/Model/DocInfo/HwpParaShape.swift similarity index 89% rename from Sources/HwpKit/Models/HwpParaShape.swift rename to Sources/HwpKit/Model/DocInfo/HwpParaShape.swift index dc83e96..15ce0de 100644 --- a/Sources/HwpKit/Models/HwpParaShape.swift +++ b/Sources/HwpKit/Model/DocInfo/HwpParaShape.swift @@ -7,7 +7,7 @@ import Foundation */ public struct HwpParaShape: HwpDataWithVersion { /**속성 1*/ - public let property1:UInt32 + public let property1: UInt32 /**왼쪽 여백*/ public let marginLeft: Int32 /**오른쪽 여백*/ @@ -19,7 +19,7 @@ public struct HwpParaShape: HwpDataWithVersion { /**문단 간격 아래*/ public let paragraphSpacingBottom: Int32 /**줄 간격. 한글 2007 이하 버전(5.0.2.5 버전 미만)에서 사용.*/ - public let lineSpacing:Int32 + public let lineSpacing: Int32 /**탭 정의 아이디(TabDef ID) 참조 값*/ public let tabDefId: UInt16 /**번호 문단 ID(Numbering ID) 또는 글머리표 문단 모양 ID(Bullet ID) 참조 값*/ @@ -35,12 +35,12 @@ public struct HwpParaShape: HwpDataWithVersion { /**문단 테두리 아래쪽 간격*/ public let borderSpacingBottom: Int16 /**속성 2(표 40 참조) (5.0.1.7 버전 이상)*/ - public var property2: UInt32? = nil + public var property2: UInt32? /**속성 3(표 41 참조) (5.0.2.5 버전 이상)*/ - public var property3: UInt32? = nil + public var property3: UInt32? /**줄 간격(5.0.2.5 버전 이상)*/ - public var lineSpacing2: UInt32? = nil - + public var lineSpacing2: UInt32? + init(_ data: Data, _ version: HwpVersion) throws { var reader = DataReader(data) defer { @@ -60,10 +60,10 @@ public struct HwpParaShape: HwpDataWithVersion { borderSpacingRight = reader.read(Int16.self) borderSpacingTop = reader.read(Int16.self) borderSpacingBottom = reader.read(Int16.self) - if version >= HwpVersion(5,0,1,7) { + if version >= HwpVersion(5, 0, 1, 7) { property2 = reader.read(UInt32.self) } - if version >= HwpVersion(5,0,2,5) { + if version >= HwpVersion(5, 0, 2, 5) { property3 = reader.read(UInt32.self) lineSpacing2 = reader.read(UInt32.self) } diff --git a/Sources/HwpKit/Models/HwpVersion.swift b/Sources/HwpKit/Model/DocInfo/HwpVersion.swift similarity index 100% rename from Sources/HwpKit/Models/HwpVersion.swift rename to Sources/HwpKit/Model/DocInfo/HwpVersion.swift diff --git a/Sources/HwpKit/Model/Section/HwpChar.swift b/Sources/HwpKit/Model/Section/HwpChar.swift new file mode 100644 index 0000000..428f881 --- /dev/null +++ b/Sources/HwpKit/Model/Section/HwpChar.swift @@ -0,0 +1,6 @@ +import Foundation + +public struct HwpChar: Codable { + public let type: HwpCharType + public let value: WCHAR +} diff --git a/Sources/HwpKit/Model/Section/HwpCtrlHeader.swift b/Sources/HwpKit/Model/Section/HwpCtrlHeader.swift new file mode 100644 index 0000000..6b9dd62 --- /dev/null +++ b/Sources/HwpKit/Model/Section/HwpCtrlHeader.swift @@ -0,0 +1,16 @@ +import Foundation + +/** + 컨트롤 헤더 + + 컨트롤 문자가 존재하면 컨트롤 문자로부터 존재하는 컨트롤 정보를 생성한다. + Tag ID : HWPTAG_CTRL_HEADER + */ +public struct HwpCtrlHeader: HwpData { + public let ctrlId: UInt32 + + init(_ data: Data) throws { + var reader = DataReader(data) + ctrlId = reader.read(UInt32.self) + } +} diff --git a/Sources/HwpKit/Model/Section/HwpListHeader.swift b/Sources/HwpKit/Model/Section/HwpListHeader.swift new file mode 100644 index 0000000..c774dcf --- /dev/null +++ b/Sources/HwpKit/Model/Section/HwpListHeader.swift @@ -0,0 +1,26 @@ +import Foundation + +/** + 문단 리스트 헤더 + + Tag ID : HWPTAG_LIST_HEADER + */ +public struct HwpListHeader: HwpData { + /** + 문단 수 + + 한글문서에선 Int16으로 되어있으나 대부분의 경우 Int32 으로 읽어야 문제가 없다 + */ + public let paragraphCount: Int32 + public let property: UInt32 + + init(_ data: Data) throws { + var reader = DataReader(data) + defer { + precondition(reader.isEOF()) + } + + paragraphCount = reader.read(Int32.self) + property = reader.read(UInt32.self) + } +} diff --git a/Sources/HwpKit/Model/Section/HwpParaCharShape.swift b/Sources/HwpKit/Model/Section/HwpParaCharShape.swift new file mode 100644 index 0000000..333d2bd --- /dev/null +++ b/Sources/HwpKit/Model/Section/HwpParaCharShape.swift @@ -0,0 +1,29 @@ +import Foundation + +/** + 문단의 글자 모양 + + Tag ID : HWPTAG_PARA_CHAR_SHAPE + */ +public struct HwpParaCharShape: HwpData { + /**글자 모양이 바뀌는 시작 위치*/ + public var startingIndex: [UInt32] + /**글자 모양 ID*/ + public var shapeId: [UInt32] + + init(_ data: Data) throws { + var reader = DataReader(data) + defer { + precondition(reader.isEOF()) + } + + var startingIndex = [UInt32]() + var shapeId = [UInt32]() + while !reader.isEOF() { + startingIndex.append(reader.read(UInt32.self)) + shapeId.append(reader.read(UInt32.self)) + } + self.startingIndex = startingIndex + self.shapeId = shapeId + } +} diff --git a/Sources/HwpKit/Model/Section/HwpParaHeader.swift b/Sources/HwpKit/Model/Section/HwpParaHeader.swift new file mode 100644 index 0000000..3a680a8 --- /dev/null +++ b/Sources/HwpKit/Model/Section/HwpParaHeader.swift @@ -0,0 +1,60 @@ +import Foundation + +/** + 문단 헤더 + + Tag ID : HWPTAG_PARA_HEADER + 텍스트의 수가 1 이상이면 문자 수만큼 텍스트를 로드하고 그렇지 않을 경우 PARA_BREAK로 문단을 생성한다. + */ +public struct HwpParaHeader: HwpDataWithVersion { + /**if (nchars & 0x80000000) { nchars &= 0x7fffffff;}*/ + public let isLastInList: Bool + /**text(=chars)*/ + public let charCount: UInt32 + /** + control mask + + (UINT32)(1<= HwpVersion(5, 0, 3, 2) { + isTraceChange = reader.read(UInt16.self) + } + } +} diff --git a/Sources/HwpKit/Model/Section/HwpParaLineSeg.swift b/Sources/HwpKit/Model/Section/HwpParaLineSeg.swift new file mode 100644 index 0000000..7ad1329 --- /dev/null +++ b/Sources/HwpKit/Model/Section/HwpParaLineSeg.swift @@ -0,0 +1,56 @@ +import Foundation + +/** + 문단의 레이아웃 + + Tag ID : HWPTAG_PARA_LINE_SEG + 문단의 각 줄을 출력할 때 사용한 Cache 정보이며, 문단 정보의 ‘각 줄에 대한 align에 대한 정보 수’만큼 반복한다. + */ +public struct HwpParaLineSeg: HwpData { + /**텍스트 시작 위치*/ + public let textStartingIndex: UInt32 + /**줄의 세로 위치*/ + public let lineLocation: Int32 + /**줄의 높이*/ + public let lineHeight: Int32 + /**텍스트 부분의 높이*/ + public let textHeight: Int32 + /**줄의 세로 위치에서 베이스라인까지 거리*/ + public let baselineDistance: Int32 + /**줄간격*/ + public let lineSpacing: Int32 + /**컬럼에서의 시작 위치*/ + public let startingLocation: Int32 + /**세그먼트의 폭*/ + public let width: Int32 + /** + 태그 + - bit 0 : 페이지의 첫 줄인지 여부 + - bit 1 : 컬럼의 첫 줄인지 여부 + - bit 16 : 텍스트가 배열되지 않은 빈 세그먼 트인지 여부 + - bit 17 : 줄의 첫 세그먼트인지 여부 + - bit 18 : 줄의 마지막 세그먼트인지 여부 + - bit 19 : 줄의 마지막에 auto-hyphenation이 수행되었는지 여부. + - bit 20 : indentation 적용 + - bit 21 : 문단 머리 모양 적용 + - bit 31 : 구현상의 편의를 위한 property + */ + public let property: UInt32 + + init(_ data: Data) throws { + var reader = DataReader(data) + defer { + // precondition(reader.isEOF()) + } + + textStartingIndex = reader.read(UInt32.self) + lineLocation = reader.read(Int32.self) + lineHeight = reader.read(Int32.self) + textHeight = reader.read(Int32.self) + baselineDistance = reader.read(Int32.self) + lineSpacing = reader.read(Int32.self) + startingLocation = reader.read(Int32.self) + width = reader.read(Int32.self) + property = reader.read(UInt32.self) + } +} diff --git a/Sources/HwpKit/Model/Section/HwpParaRangeTag.swift b/Sources/HwpKit/Model/Section/HwpParaRangeTag.swift new file mode 100644 index 0000000..6a8cd1a --- /dev/null +++ b/Sources/HwpKit/Model/Section/HwpParaRangeTag.swift @@ -0,0 +1,32 @@ +import Foundation + +/** + 문단의 영역 태그 + + range tag 정보를 정보 수만큼 읽어 온다. range tag는 텍스트의 일정 영역을 마킹하는 용도로 사용되며, + 글자 모양과는 달리 각 영역은 서로 겹칠 수 있다.(형광펜, 교정 부호 등) + Tag ID : HWPTAG_PARA_RANGE_TAG + */ +public struct HwpParaRangeTag: HwpData { + /**영역 시작*/ + public let start: UInt32 + /**영역 끝*/ + public let end: UInt32 + /** + 태그(종류 + 데이터) + + 상위 8비트가 종류를 하위 24비트가 종류별로 다른 설명을 부여할 수 있는 임의의 데이터를 나타낸다. + */ + public let tag: UInt32 + + init(_ data: Data) throws { + var reader = DataReader(data) + defer { + precondition(reader.isEOF()) + } + + start = reader.read(UInt32.self) + end = reader.read(UInt32.self) + tag = reader.read(UInt32.self) + } +} diff --git a/Sources/HwpKit/Model/Section/HwpParaText.swift b/Sources/HwpKit/Model/Section/HwpParaText.swift new file mode 100644 index 0000000..56bd650 --- /dev/null +++ b/Sources/HwpKit/Model/Section/HwpParaText.swift @@ -0,0 +1,34 @@ +import Foundation + +/** + 문단의 텍스트 + + Tag ID : HWPTAG_PARA_TEXT + 문단은 최소 하나의 문자 Shape buffer가 존재하며, 첫 번째 pos가 반드시 0이어야 한다. + 텍스트 문자 Shape 레코드를 글자 모양 정보 수(Character Shapes)만큼 읽는다. + */ +public struct HwpParaText: HwpData { + /**문자수만큼의 텍스트*/ + public var charArray: [HwpChar] + + init(_ data: Data) throws { + var reader = DataReader(data) + var array = [HwpChar]() + while !reader.isEOF() { + let char = reader.read(WCHAR.self) + switch char { + case 0, 1, 13: + array.append(HwpChar(type: .char, value: char)) + case 4...9, 19...20: + array.append(HwpChar(type: .inline, value: char)) + reader.readBytes(14) + case 1...3, 11...12, 14...18, 21...23: + array.append(HwpChar(type: .extended, value: char)) + reader.readBytes(14) + default: + array.append(HwpChar(type: .char, value: char)) + } + } + charArray = array + } +} diff --git a/Sources/HwpKit/Model/Section/HwpParagraph.swift b/Sources/HwpKit/Model/Section/HwpParagraph.swift new file mode 100644 index 0000000..d1b0000 --- /dev/null +++ b/Sources/HwpKit/Model/Section/HwpParagraph.swift @@ -0,0 +1,42 @@ +import Foundation + +public struct HwpParagraph: HwpRecordWithVersion { + public let paraHeader: HwpParaHeader + public var paraText: HwpParaText? + public var paraCharShape: HwpParaCharShape? + + public var paraLineSegArray: [HwpParaLineSeg]? + public var paraRangeTagArray: [HwpParaRangeTag]? + public var ctrlHeaderArray: [HwpCtrlHeader]? + public var listHeaderArray: [HwpListHeader]? + + init(_ record: HwpTreeRecord, _ version: HwpVersion) throws { + paraHeader = try HwpParaHeader(record.payload, version) + + if let paraText = record.children + .first(where: {$0.tagId == HwpSectionTag.paraText}) { + self.paraText = try HwpParaText(paraText.payload) + } + + if let paraCharShape = record.children + .first(where: {$0.tagId == HwpSectionTag.paraCharShape}) { + self.paraCharShape = try HwpParaCharShape(paraCharShape.payload) + } + + paraLineSegArray = try record.children + .filter {$0.tagId == HwpSectionTag.paraLineSeg} + .map {try HwpParaLineSeg($0.payload)} + + paraRangeTagArray = try record.children + .filter {$0.tagId == HwpSectionTag.paraRangeTag} + .map {try HwpParaRangeTag($0.payload)} + + ctrlHeaderArray = try record.children + .filter {$0.tagId == HwpSectionTag.ctrlHeader} + .map {try HwpCtrlHeader($0.payload)} + + listHeaderArray = try record.children + .filter {$0.tagId == HwpSectionTag.listHeader} + .map { try HwpListHeader($0.payload)} + } +} diff --git a/Sources/HwpKit/Streams/HwpDocInfo.swift b/Sources/HwpKit/Streams/HwpDocInfo.swift index a264be1..10fe107 100644 --- a/Sources/HwpKit/Streams/HwpDocInfo.swift +++ b/Sources/HwpKit/Streams/HwpDocInfo.swift @@ -23,33 +23,33 @@ public struct HwpDocInfo: HwpDataWithVersion { // TODO HWPTAG_LAYOUT_COMPATIBILITY init(_ data: Data, _ version: HwpVersion) throws { - let records = try parseRecordTree(data: data) + let records = try parseRecordArray(data: data) guard let documentProperties = records - .first(where: {$0.tagId == HwpDocInfoTag.DOCUMENT_PROPERTIES}) + .first(where: {$0.tagId == HwpDocInfoTag.documentProperties}) else { - throw HwpError.recordDoesNotExist(tag: HwpDocInfoTag.DOCUMENT_PROPERTIES) + throw HwpError.recordDoesNotExist(tag: HwpDocInfoTag.documentProperties) } self.documentProperties = HwpDocInfo.visitDocumentPropertes(documentProperties) binDataArray = try records - .filter {$0.tagId == HwpDocInfoTag.BIN_DATA} + .filter {$0.tagId == HwpDocInfoTag.binData} .map {try HwpBinData($0.payload)} faceNameArray = try records - .filter {$0.tagId == HwpDocInfoTag.FACE_NAME} + .filter {$0.tagId == HwpDocInfoTag.faceName} .map {try HwpFaceName($0.payload)} borderFillArray = try records - .filter {$0.tagId == HwpDocInfoTag.BORDER_FILL} + .filter {$0.tagId == HwpDocInfoTag.borderFill} .map {try HwpBorderFill($0.payload)} charShapeArray = try records - .filter {$0.tagId == HwpDocInfoTag.CHAR_SHAPE} + .filter {$0.tagId == HwpDocInfoTag.charShape} .map {try HwpCharShape($0.payload, version)} - + paraShapeArray = try records - .filter {$0.tagId == HwpDocInfoTag.PARA_SHAPE} + .filter {$0.tagId == HwpDocInfoTag.paraShape} .map {try HwpParaShape($0.payload, version)} } diff --git a/Sources/HwpKit/Streams/HwpSection.swift b/Sources/HwpKit/Streams/HwpSection.swift index 940d647..b849ac3 100644 --- a/Sources/HwpKit/Streams/HwpSection.swift +++ b/Sources/HwpKit/Streams/HwpSection.swift @@ -1,10 +1,16 @@ -struct HwpSection { - let width: Int = 0 - let height: Int = 0 - let paddingLeft: Int = 0 - let paddingRight: Int = 0 - let paddingTop: Int = 0 - let paddingBottom: Int = 0 - let headerPadding: Int = 0 - let footerPadding: Int = 0 +import Foundation + +/** + 본문 + */ +public struct HwpSection: HwpDataWithVersion { + public var paragraph: [HwpParagraph] + + init(_ data: Data, _ version: HwpVersion) throws { + let records = parseTreeRecord(data: data) + paragraph = try records.children.map {record in + precondition(record.tagId == HwpSectionTag.paraHeader) + return try HwpParagraph(record, version) + } + } } diff --git a/Sources/HwpKit/Streams/HwpSummary.swift b/Sources/HwpKit/Streams/HwpSummary.swift index ac52a07..c17a194 100644 --- a/Sources/HwpKit/Streams/HwpSummary.swift +++ b/Sources/HwpKit/Streams/HwpSummary.swift @@ -3,7 +3,7 @@ import Foundation /** 문서 요약 - \005HwpSummaryInfomation 스트림에는 한글 메뉴의 “파일-문서 정보-문서 요약”에서 입력한 내 용이 저장된다. + \005HwpSummaryInfomation 스트림에는 한글 메뉴의 “파일-문서 정보-문서 요약”에서 입력한 내용이 저장된다. */ struct HwpSummary { let title: String diff --git a/Sources/HwpKit/Utils/DataReader.swift b/Sources/HwpKit/Utils/DataReader.swift index 624e3e5..9680a62 100644 --- a/Sources/HwpKit/Utils/DataReader.swift +++ b/Sources/HwpKit/Utils/DataReader.swift @@ -12,7 +12,7 @@ struct DataReader { return offset == data.count } - mutating func readBytes(_ length: Int) -> Data { + @discardableResult mutating func readBytes(_ length: Int) -> Data { precondition(offset + length < data.count + 1) defer { offset += length diff --git a/Sources/HwpKit/Utils/HwpColor.swift b/Sources/HwpKit/Utils/HwpColor.swift index 95ee191..0ef6aba 100644 --- a/Sources/HwpKit/Utils/HwpColor.swift +++ b/Sources/HwpKit/Utils/HwpColor.swift @@ -1,22 +1,20 @@ import Foundation public struct HwpColor: Codable, Equatable { - // swiftlint:disable identifier_name - public let r: Int - public let g: Int - public let b: Int - // swiftlint:enable identifier_name + public let red: Int + public let green: Int + public let blue: Int public init(_ data: UInt32) { - r = getBitValue(Int(data), 0, 7) - g = getBitValue(Int(data), 0, 7) - b = getBitValue(Int(data), 0, 7) + red = getBitValue(Int(data), 0, 7) + green = getBitValue(Int(data), 0, 7) + blue = getBitValue(Int(data), 0, 7) } public init(_ red: Int, _ green: Int, _ blue: Int) { - self.r = red - self.g = green - self.b = blue + self.red = red + self.green = green + self.blue = blue } } diff --git a/Sources/HwpKit/Utils/HwpData.swift b/Sources/HwpKit/Utils/HwpData.swift index 7c27fc6..f9c163e 100644 --- a/Sources/HwpKit/Utils/HwpData.swift +++ b/Sources/HwpKit/Utils/HwpData.swift @@ -3,3 +3,11 @@ import Foundation protocol HwpData: Codable { init(_ data: Data) throws } + +protocol HwpDataWithVersion: Codable { + init(_ data: Data, _ version: HwpVersion) throws +} + +protocol HwpRecordWithVersion: Codable { + init(_ record: HwpTreeRecord, _ version: HwpVersion) throws +} diff --git a/Sources/HwpKit/Utils/HwpDataWithVersion.swift b/Sources/HwpKit/Utils/HwpDataWithVersion.swift deleted file mode 100644 index 9e3f393..0000000 --- a/Sources/HwpKit/Utils/HwpDataWithVersion.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Foundation - -protocol HwpDataWithVersion: Codable { - init(_ data: Data, _ version: HwpVersion) throws -} diff --git a/Sources/HwpKit/Utils/HwpRecord.swift b/Sources/HwpKit/Utils/HwpRecord.swift index 847ebd8..e60851e 100644 --- a/Sources/HwpKit/Utils/HwpRecord.swift +++ b/Sources/HwpKit/Utils/HwpRecord.swift @@ -26,7 +26,7 @@ struct HwpRecord { let payload: Data } -func parseRecordTree(data: Data) throws -> [HwpRecord] { +func parseRecordArray(data: Data) throws -> [HwpRecord] { var records = [HwpRecord]() var reader = DataReader(data) @@ -39,7 +39,6 @@ func parseRecordTree(data: Data) throws -> [HwpRecord] { if size == 0xFFF { size = reader.read(UInt32.self) } - print(tagId, level, size) let payload = reader.readBytes(Int(size)) let current = HwpRecord(tagId: tagId, level: level, payload: payload) diff --git a/Sources/HwpKit/Utils/HwpTreeRecord.swift b/Sources/HwpKit/Utils/HwpTreeRecord.swift new file mode 100644 index 0000000..743ce74 --- /dev/null +++ b/Sources/HwpKit/Utils/HwpTreeRecord.swift @@ -0,0 +1,61 @@ +import Foundation + +/** + 데이터 레코드 구조 + + 논리적으로 연관된 데이터들을 헤더 정보와 함께 저장하는 방식을 데이터 레코드라고 한다. + 레코드 구조를 가지는 스트림은 연속된 여러 개의 레코드로 구성된다. 데이터 레코드는 헤더와 데이터로 구성되며 각 헤더 정보를 활용하여 전체 논리적 구조를 생성하게 된다. + 레코드의 헤더에는 데이터 확장에 대비한 정보를 가지고 있다. + 따라서 이후에 한글의 기능이 확장되어 레코드에 데이터가 추가되는 경우에도 하위 버전의 한글이 상위 버전의 한글 문서를 읽을 수 있도록 하위 호환성이 보장된다. + */ +class HwpTreeRecord { + /** + 레코드가 나타내는 데이터의 종류를 나타내는 태그이다. Tag ID에는 10 비트가 사용되므 로 0x000 - 0x3FF까지 가능하다. + - 0x000 - 0x00F = 일반 레코드 태그가 아닌 특별한 용도로 사용한다. + - 0x010 - 0x1FF = 한글에 의해 내부용으로 예약된 영역(HWPTAG_BEGIN = 0x010) + - 0x200 - 0x3FF = 외부 어플리케이션이 사용할 수 있는 영역 + */ + let tagId: UInt32 + /** + 연관된 레코드의 논리적인 묶음을 표현하기 위한 정보 + + 대부분 하나의 오브젝트는 여러 개의 레코드로 구성되는 것이 일반적이기 때문에 + 하나의 레코드가 아닌 "논리적으로 연관된 연속된 레코드"라는 개념이 필요하다. + 스트림을 구성하는 모든 레코드는 계층 구조로 표현할 수 있는데, 레벨은 바로 이 계층 구조에서의 depth를 나타낸다. + */ + let level: UInt32 + let payload: Data + var children: [HwpTreeRecord] + + init(tagId: UInt32, level: UInt32, payload: Data) { + self.tagId = tagId + self.level = level + self.payload = payload + self.children = [HwpTreeRecord]() + } +} + +func parseTreeRecord(data: Data) -> HwpTreeRecord { + var reader = DataReader(data) + let root = HwpTreeRecord(tagId: 0, level: 0, payload: Data()) + + while !reader.isEOF() { + let value = reader.read(UInt32.self) + let tagId = value & 0x3FF + let level = (value >> 10) & 0x3FF + var size = (value >> 20) & 0xFFF + if size == 0xFFF { + size = reader.read(UInt32.self) + } + let payload = reader.readBytes(Int(size)) + + var parent = root + + for _ in 0.. [Data] { + guard let storage = streams[streamName.rawValue] else { + throw HwpError.streamDoesNotExist(name: streamName) + } + return try storage.children.map { try readData($0, isCompressed) } + } + + private func readData(_ stream: DirectoryEntry, _ isCompressed: Bool) throws -> Data { let reader = try ole.stream(stream) let data = reader.readDataToEnd() if isCompressed { guard let decompressedData = data.decompress(withAlgorithm: .zlib) else { - throw HwpError.streamDecompressFailed(name: streamName) + throw HwpError.streamDecompressFailed(name: HwpStreamName(rawValue: stream.name)!) } return decompressedData } else { diff --git a/Tests/HwpKitTests/Basic/BlankTests.swift b/Tests/HwpKitTests/Blank/BlankTests.swift similarity index 100% rename from Tests/HwpKitTests/Basic/BlankTests.swift rename to Tests/HwpKitTests/Blank/BlankTests.swift diff --git a/Tests/HwpKitTests/Basic/blank.hwp b/Tests/HwpKitTests/Blank/blank.hwp similarity index 100% rename from Tests/HwpKitTests/Basic/blank.hwp rename to Tests/HwpKitTests/Blank/blank.hwp diff --git a/Tests/HwpKitTests/Basic/NooriTests.swift b/Tests/HwpKitTests/Noori/NooriDocInfoTests.swift similarity index 77% rename from Tests/HwpKitTests/Basic/NooriTests.swift rename to Tests/HwpKitTests/Noori/NooriDocInfoTests.swift index ffe3722..338426a 100644 --- a/Tests/HwpKitTests/Basic/NooriTests.swift +++ b/Tests/HwpKitTests/Noori/NooriDocInfoTests.swift @@ -1,7 +1,7 @@ import HwpKit import XCTest -final class NooriTests: XCTestCase { +final class NooriDocInfoTests: XCTestCase { func openHwp() throws -> HwpFile { let url = URL(fileURLWithPath: #file) .deletingLastPathComponent() @@ -63,25 +63,25 @@ final class NooriTests: XCTestCase { let border = hwp.docInfo.borderFillArray XCTAssertEqual(border[0].borderColor[0], HwpColor(0, 0, 0)) } - + func testCharShape() throws { let hwp = try openHwp() let char = hwp.docInfo.charShapeArray XCTAssertEqual(char[0].property, 0) - XCTAssertEqual(char[0].faceColor, HwpColor(0,0,0)) + XCTAssertEqual(char[0].faceColor, HwpColor(0, 0, 0)) XCTAssertEqual(char[0].borderFillId, 2) - XCTAssertEqual(char[0].faceId, [5,5,5,5,5,5,5]) - XCTAssertEqual(char[0].faceLocation, [0,0,0,0,0,0,0]) + XCTAssertEqual(char[0].faceId, [5, 5, 5, 5, 5, 5, 5]) + XCTAssertEqual(char[0].faceLocation, [0, 0, 0, 0, 0, 0, 0]) XCTAssertEqual(char[0].faceRelativeSize, Array(repeating: 100, count: 7)) XCTAssertEqual(char[0].faceScaleX, Array(repeating: 100, count: 7)) - XCTAssertEqual(char[0].shadeColor, HwpColor(255,255,255)) - XCTAssertEqual(char[0].shadowColor, HwpColor(178,178,178)) - XCTAssertEqual(char[0].underlineColor, HwpColor(0,0,0)) - XCTAssertEqual(char[0].strikethroughColor!, HwpColor(0,0,0)) - + XCTAssertEqual(char[0].shadeColor, HwpColor(255, 255, 255)) + XCTAssertEqual(char[0].shadowColor, HwpColor(178, 178, 178)) + XCTAssertEqual(char[0].underlineColor, HwpColor(0, 0, 0)) + XCTAssertEqual(char[0].strikethroughColor!, HwpColor(0, 0, 0)) + XCTAssertEqual(char[58].property, 2) } - + func testTabDef() throws { let hwp = try openHwp() let shape = hwp.docInfo.paraShapeArray @@ -89,10 +89,20 @@ final class NooriTests: XCTestCase { XCTAssertEqual(shape[46].property1, 268) } + func testCtrlHeader() throws { + let hwp = try openHwp() + XCTAssertEqual(hwp.sectionArray[0].paragraph[2].ctrlHeaderArray![0].ctrlId, 1885826672) + } + static var allTests = [ ("testSectionSize", testSectionSize), ("testStartingIndex", testStartingIndex), ("testCaratLocation", testCaratLocation), - ("testBinData", testBinData) + ("testBinData", testBinData), + ("testFaceName", testFaceName), + ("testBorderFill", testBorderFill), + ("testCharShape", testCharShape), + ("testTabDef", testTabDef), + ("testCtrlHeader", testCtrlHeader) ] } diff --git a/Tests/HwpKitTests/Noori/NooriSectionTests.swift b/Tests/HwpKitTests/Noori/NooriSectionTests.swift new file mode 100644 index 0000000..820a3fb --- /dev/null +++ b/Tests/HwpKitTests/Noori/NooriSectionTests.swift @@ -0,0 +1,63 @@ +import HwpKit +import XCTest + +final class NooriSectionTests: XCTestCase { + func openHwp() throws -> HwpFile { + let url = URL(fileURLWithPath: #file) + .deletingLastPathComponent() + .appendingPathComponent("noori.hwp") + return try HwpFile(filePath: url.path) + } + + func testParagraph() throws { + let hwp = try openHwp() + for index in 0...20 { + XCTAssertNotNil(hwp.sectionArray[0].paragraph[index]) + } + } + + func testParaText() throws { + let hwp = try openHwp() + let text = hwp.sectionArray[0].paragraph[0].paraText + XCTAssertEqual(text!.charArray[0].type, .extended) + XCTAssertEqual(text!.charArray[3].type, .char) + + XCTAssertEqual(text!.charArray[0].value, 2) + XCTAssertEqual(text!.charArray[2].value, 11) + XCTAssertEqual(text!.charArray[3].value, 13) + + XCTAssertNil(hwp.sectionArray[0].paragraph[15].paraText) + XCTAssertNil(hwp.sectionArray[0].paragraph[16].paraText) + XCTAssertNil(hwp.sectionArray[0].paragraph[17].paraText) + } + + func testParaCharShape() throws { + let hwp = try openHwp() + XCTAssertEqual(hwp.sectionArray[0].paragraph[0].paraCharShape?.startingIndex[0], 0) + XCTAssertEqual(hwp.sectionArray[0].paragraph[0].paraCharShape?.shapeId[0], 19) + XCTAssertEqual(hwp.sectionArray[0].paragraph[20].paraCharShape?.startingIndex[0], 0) + XCTAssertEqual(hwp.sectionArray[0].paragraph[20].paraCharShape?.shapeId[0], 30) + } + + func testParaLineSeg() throws { + let hwp = try openHwp() + let seg0 = hwp.sectionArray[0].paragraph[0].paraLineSegArray![0] + XCTAssertEqual(seg0.textStartingIndex, 0) + XCTAssertEqual(seg0.lineLocation, 0) + XCTAssertEqual(seg0.lineHeight, 6134) + XCTAssertEqual(seg0.textHeight, 6134) + XCTAssertEqual(seg0.baselineDistance, 5214) + XCTAssertEqual(seg0.lineSpacing, 840) + XCTAssertEqual(seg0.startingLocation, 0) + XCTAssertEqual(seg0.width, 48188) + + XCTAssertNotNil(hwp.sectionArray[0].paragraph[20].paraLineSegArray![0]) + } + + static var allTests = [ + ("testParagraph", testParagraph), + ("testParaText", testParaText), + ("testParaCharShape", testParaCharShape), + ("testParaLineSeg", testParaLineSeg) + ] +} diff --git a/Tests/HwpKitTests/Basic/noori.hwp b/Tests/HwpKitTests/Noori/noori.hwp similarity index 100% rename from Tests/HwpKitTests/Basic/noori.hwp rename to Tests/HwpKitTests/Noori/noori.hwp diff --git a/Tests/HwpKitTests/XCTestManifests.swift b/Tests/HwpKitTests/XCTestManifests.swift index 360d9ae..22834bb 100644 --- a/Tests/HwpKitTests/XCTestManifests.swift +++ b/Tests/HwpKitTests/XCTestManifests.swift @@ -4,6 +4,8 @@ import XCTest public func allTests() -> [XCTestCaseEntry] { [ testCase(BasicTests.allTests), + testCase(NooriDocInfoTests.allTests), + testCase(NooriSectionTests.allTests), testCase(VersionTests.allTests), testCase(HwpUtilTests.allTests) ] diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index cf4f8b8..521339c 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1,5 +1,4 @@ import XCTest - import HwpKitTests var tests = [XCTestCaseEntry]()