1#
2# The Python Imaging Library.
3#
4# MSP file handling
5#
6# This is the format used by the Paint program in Windows 1 and 2.
7#
8# History:
9# 95-09-05 fl Created
10# 97-01-03 fl Read/write MSP images
11# 17-02-21 es Fixed RLE interpretation
12#
13# Copyright (c) Secret Labs AB 1997.
14# Copyright (c) Fredrik Lundh 1995-97.
15# Copyright (c) Eric Soroos 2017.
16#
17# See the README file for information on usage and redistribution.
18#
19# More info on this format: https://archive.org/details/gg243631
20# Page 313:
21# Figure 205. Windows Paint Version 1: "DanM" Format
22# Figure 206. Windows Paint Version 2: "LinS" Format. Used in Windows V2.03
23#
24# See also: https://www.fileformat.info/format/mspaint/egff.htm
25from __future__ import annotations
26
27import io
28import struct
29from typing import IO
30
31from . import Image, ImageFile
32from ._binary import i16le as i16
33from ._binary import o16le as o16
34
35#
36# read MSP files
37
38
39def _accept(prefix: bytes) -> bool:
40 return prefix[:4] in [b"DanM", b"LinS"]
41
42
43##
44# Image plugin for Windows MSP images. This plugin supports both
45# uncompressed (Windows 1.0).
46
47
48class MspImageFile(ImageFile.ImageFile):
49 format = "MSP"
50 format_description = "Windows Paint"
51
52 def _open(self) -> None:
53 # Header
54 assert self.fp is not None
55
56 s = self.fp.read(32)
57 if not _accept(s):
58 msg = "not an MSP file"
59 raise SyntaxError(msg)
60
61 # Header checksum
62 checksum = 0
63 for i in range(0, 32, 2):
64 checksum = checksum ^ i16(s, i)
65 if checksum != 0:
66 msg = "bad MSP checksum"
67 raise SyntaxError(msg)
68
69 self._mode = "1"
70 self._size = i16(s, 4), i16(s, 6)
71
72 if s[:4] == b"DanM":
73 self.tile = [("raw", (0, 0) + self.size, 32, ("1", 0, 1))]
74 else:
75 self.tile = [("MSP", (0, 0) + self.size, 32, None)]
76
77
78class MspDecoder(ImageFile.PyDecoder):
79 # The algo for the MSP decoder is from
80 # https://www.fileformat.info/format/mspaint/egff.htm
81 # cc-by-attribution -- That page references is taken from the
82 # Encyclopedia of Graphics File Formats and is licensed by
83 # O'Reilly under the Creative Common/Attribution license
84 #
85 # For RLE encoded files, the 32byte header is followed by a scan
86 # line map, encoded as one 16bit word of encoded byte length per
87 # line.
88 #
89 # NOTE: the encoded length of the line can be 0. This was not
90 # handled in the previous version of this encoder, and there's no
91 # mention of how to handle it in the documentation. From the few
92 # examples I've seen, I've assumed that it is a fill of the
93 # background color, in this case, white.
94 #
95 #
96 # Pseudocode of the decoder:
97 # Read a BYTE value as the RunType
98 # If the RunType value is zero
99 # Read next byte as the RunCount
100 # Read the next byte as the RunValue
101 # Write the RunValue byte RunCount times
102 # If the RunType value is non-zero
103 # Use this value as the RunCount
104 # Read and write the next RunCount bytes literally
105 #
106 # e.g.:
107 # 0x00 03 ff 05 00 01 02 03 04
108 # would yield the bytes:
109 # 0xff ff ff 00 01 02 03 04
110 #
111 # which are then interpreted as a bit packed mode '1' image
112
113 _pulls_fd = True
114
115 def decode(self, buffer: bytes) -> tuple[int, int]:
116 assert self.fd is not None
117
118 img = io.BytesIO()
119 blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8))
120 try:
121 self.fd.seek(32)
122 rowmap = struct.unpack_from(
123 f"<{self.state.ysize}H", self.fd.read(self.state.ysize * 2)
124 )
125 except struct.error as e:
126 msg = "Truncated MSP file in row map"
127 raise OSError(msg) from e
128
129 for x, rowlen in enumerate(rowmap):
130 try:
131 if rowlen == 0:
132 img.write(blank_line)
133 continue
134 row = self.fd.read(rowlen)
135 if len(row) != rowlen:
136 msg = f"Truncated MSP file, expected {rowlen} bytes on row {x}"
137 raise OSError(msg)
138 idx = 0
139 while idx < rowlen:
140 runtype = row[idx]
141 idx += 1
142 if runtype == 0:
143 (runcount, runval) = struct.unpack_from("Bc", row, idx)
144 img.write(runval * runcount)
145 idx += 2
146 else:
147 runcount = runtype
148 img.write(row[idx : idx + runcount])
149 idx += runcount
150
151 except struct.error as e:
152 msg = f"Corrupted MSP file in row {x}"
153 raise OSError(msg) from e
154
155 self.set_as_raw(img.getvalue(), ("1", 0, 1))
156
157 return -1, 0
158
159
160Image.register_decoder("MSP", MspDecoder)
161
162
163#
164# write MSP files (uncompressed only)
165
166
167def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
168 if im.mode != "1":
169 msg = f"cannot write mode {im.mode} as MSP"
170 raise OSError(msg)
171
172 # create MSP header
173 header = [0] * 16
174
175 header[0], header[1] = i16(b"Da"), i16(b"nM") # version 1
176 header[2], header[3] = im.size
177 header[4], header[5] = 1, 1
178 header[6], header[7] = 1, 1
179 header[8], header[9] = im.size
180
181 checksum = 0
182 for h in header:
183 checksum = checksum ^ h
184 header[12] = checksum # FIXME: is this the right field?
185
186 # header
187 for h in header:
188 fp.write(o16(h))
189
190 # image body
191 ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 32, ("1", 0, 1))])
192
193
194#
195# registry
196
197Image.register_open(MspImageFile.format, MspImageFile, _accept)
198Image.register_save(MspImageFile.format, _save)
199
200Image.register_extension(MspImageFile.format, ".msp")