updates
This commit is contained in:
540
Backend/venv/bin/priplan9topng
Executable file
540
Backend/venv/bin/priplan9topng
Executable file
@@ -0,0 +1,540 @@
|
||||
#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python3
|
||||
|
||||
# Imported from //depot/prj/plan9topam/master/code/plan9topam.py#4 on
|
||||
# 2009-06-15.
|
||||
|
||||
"""Command line tool to convert from Plan 9 image format to PNG format.
|
||||
|
||||
Plan 9 image format description:
|
||||
https://plan9.io/magic/man2html/6/image
|
||||
|
||||
Where possible this tool will use unbuffered read() calls,
|
||||
so that when finished the file offset is exactly at the end of
|
||||
the image data.
|
||||
This is useful for Plan9 subfont files which place font metric
|
||||
data immediately after the image.
|
||||
"""
|
||||
|
||||
# Test materials
|
||||
|
||||
# asset/left.bit is a Plan 9 image file, a leftwards facing Glenda.
|
||||
# Other materials have to be scrounged from the internet.
|
||||
# https://plan9.io/sources/plan9/sys/games/lib/sokoban/images/cargo.bit
|
||||
|
||||
import array
|
||||
import collections
|
||||
import io
|
||||
|
||||
# http://www.python.org/doc/2.3.5/lib/module-itertools.html
|
||||
import itertools
|
||||
import os
|
||||
|
||||
# http://www.python.org/doc/2.3.5/lib/module-re.html
|
||||
import re
|
||||
import struct
|
||||
|
||||
# http://www.python.org/doc/2.3.5/lib/module-sys.html
|
||||
import sys
|
||||
|
||||
# https://docs.python.org/3/library/tarfile.html
|
||||
import tarfile
|
||||
|
||||
|
||||
# https://pypi.org/project/pypng/
|
||||
import png
|
||||
|
||||
# internal
|
||||
import prix
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
"""Some sort of Plan 9 image error."""
|
||||
|
||||
|
||||
def block(s, n):
|
||||
return zip(*[iter(s)] * n)
|
||||
|
||||
|
||||
def plan9_as_image(inp):
|
||||
"""Represent a Plan 9 image file as a png.Image instance, so
|
||||
that it can be written as a PNG file.
|
||||
Works with compressed input files and may work with uncompressed files.
|
||||
"""
|
||||
|
||||
# Use inp.raw if available.
|
||||
# This avoids buffering and means that when the image is processed,
|
||||
# the resulting input stream is cued up exactly at the end
|
||||
# of the image.
|
||||
inp = getattr(inp, "raw", inp)
|
||||
|
||||
info, blocks = plan9_open_image(inp)
|
||||
|
||||
rows, infodict = plan9_image_rows(blocks, info)
|
||||
|
||||
return png.Image(rows, infodict)
|
||||
|
||||
|
||||
def plan9_open_image(inp):
|
||||
"""Open a Plan9 image file (`inp` should be an already open
|
||||
file object), and return (`info`, `blocks`) pair.
|
||||
`info` should be a Plan9 5-tuple;
|
||||
`blocks` is the input, and it should yield (`row`, `data`)
|
||||
pairs (see :meth:`pixmeta`).
|
||||
"""
|
||||
|
||||
r = inp.read(11)
|
||||
if r == b"compressed\n":
|
||||
info, blocks = decompress(inp)
|
||||
else:
|
||||
# Since Python 3, there is a good chance that this path
|
||||
# doesn't work.
|
||||
info, blocks = glue(inp, r)
|
||||
|
||||
return info, blocks
|
||||
|
||||
|
||||
def glue(f, r):
|
||||
"""Return (info, stream) pair, given `r` the initial portion of
|
||||
the metadata that has already been read from the stream `f`.
|
||||
"""
|
||||
|
||||
r = r + f.read(60 - len(r))
|
||||
return (meta(r), f)
|
||||
|
||||
|
||||
def meta(r):
|
||||
"""Convert 60 byte bytestring `r`, the metadata from an image file.
|
||||
Returns a 5-tuple (*chan*,*minx*,*miny*,*limx*,*limy*).
|
||||
5-tuples may settle into lists in transit.
|
||||
|
||||
As per https://plan9.io/magic/man2html/6/image the metadata
|
||||
comprises 5 words separated by blanks.
|
||||
As it happens each word starts at an index that is a multiple of 12,
|
||||
but this routine does not care about that.
|
||||
"""
|
||||
|
||||
r = r.split()
|
||||
# :todo: raise FormatError
|
||||
if 5 != len(r):
|
||||
raise Error("Expected 5 space-separated words in metadata")
|
||||
r = [r[0]] + [int(x) for x in r[1:]]
|
||||
return r
|
||||
|
||||
|
||||
def bitdepthof(chan):
|
||||
"""Return the bitdepth for a Plan9 pixel format string."""
|
||||
|
||||
maxd = 0
|
||||
for c in re.findall(rb"[a-z]\d*", chan):
|
||||
if c[0] != "x":
|
||||
maxd = max(maxd, int(c[1:]))
|
||||
return maxd
|
||||
|
||||
|
||||
def maxvalof(chan):
|
||||
"""Return the netpbm MAXVAL for a Plan9 pixel format string."""
|
||||
|
||||
bitdepth = bitdepthof(chan)
|
||||
return (2 ** bitdepth) - 1
|
||||
|
||||
|
||||
def plan9_image_rows(blocks, metadata):
|
||||
"""
|
||||
Convert (uncompressed) Plan 9 image file to pair of (*rows*, *info*).
|
||||
This is intended to be used by PyPNG format.
|
||||
*info* is the image info (metadata) returned in a dictionary,
|
||||
*rows* is an iterator that yields each row in
|
||||
boxed row flat pixel format.
|
||||
|
||||
`blocks`, should be an iterator of (`row`, `data`) pairs.
|
||||
"""
|
||||
|
||||
chan, minx, miny, limx, limy = metadata
|
||||
rows = limy - miny
|
||||
width = limx - minx
|
||||
nchans = len(re.findall(b"[a-wyz]", chan))
|
||||
alpha = b"a" in chan
|
||||
# Iverson's convention for the win!
|
||||
ncolour = nchans - alpha
|
||||
greyscale = ncolour == 1
|
||||
bitdepth = bitdepthof(chan)
|
||||
maxval = maxvalof(chan)
|
||||
|
||||
# PNG style info dict.
|
||||
meta = dict(
|
||||
size=(width, rows),
|
||||
bitdepth=bitdepth,
|
||||
greyscale=greyscale,
|
||||
alpha=alpha,
|
||||
planes=nchans,
|
||||
)
|
||||
|
||||
arraycode = "BH"[bitdepth > 8]
|
||||
|
||||
return (
|
||||
map(
|
||||
lambda x: array.array(arraycode, itertools.chain(*x)),
|
||||
block(unpack(blocks, rows, width, chan, maxval), width),
|
||||
),
|
||||
meta,
|
||||
)
|
||||
|
||||
|
||||
def unpack(f, rows, width, chan, maxval):
|
||||
"""Unpack `f` into pixels.
|
||||
`chan` describes the pixel format using
|
||||
the Plan9 syntax ("k8", "r8g8b8", and so on).
|
||||
Assumes the pixel format has a total channel bit depth
|
||||
that is either a multiple or a divisor of 8
|
||||
(the Plan9 image specification requires this).
|
||||
`f` should be an iterator that returns blocks of input such that
|
||||
each block contains a whole number of pixels.
|
||||
The return value is an iterator that yields each pixel as an n-tuple.
|
||||
"""
|
||||
|
||||
def mask(w):
|
||||
"""An integer, to be used as a mask, with bottom `w` bits set to 1."""
|
||||
|
||||
return (1 << w) - 1
|
||||
|
||||
def deblock(f, depth, width):
|
||||
"""A "packer" used to convert multiple bytes into single pixels.
|
||||
`depth` is the pixel depth in bits (>= 8), `width` is the row width in
|
||||
pixels.
|
||||
"""
|
||||
|
||||
w = depth // 8
|
||||
i = 0
|
||||
for block in f:
|
||||
for i in range(len(block) // w):
|
||||
p = block[w * i : w * (i + 1)]
|
||||
i += w
|
||||
# Convert little-endian p to integer x
|
||||
x = 0
|
||||
s = 1 # scale
|
||||
for j in p:
|
||||
x += s * j
|
||||
s <<= 8
|
||||
yield x
|
||||
|
||||
def bitfunge(f, depth, width):
|
||||
"""A "packer" used to convert single bytes into multiple pixels.
|
||||
Depth is the pixel depth (< 8), width is the row width in pixels.
|
||||
"""
|
||||
|
||||
assert 8 / depth == 8 // depth
|
||||
|
||||
for block in f:
|
||||
col = 0
|
||||
for x in block:
|
||||
for j in range(8 // depth):
|
||||
yield x >> (8 - depth)
|
||||
col += 1
|
||||
if col == width:
|
||||
# A row-end forces a new byte even if
|
||||
# we haven't consumed all of the current byte.
|
||||
# Effectively rows are bit-padded to make
|
||||
# a whole number of bytes.
|
||||
col = 0
|
||||
break
|
||||
x <<= depth
|
||||
|
||||
# number of bits in each channel
|
||||
bits = [int(d) for d in re.findall(rb"\d+", chan)]
|
||||
# colr of each channel
|
||||
# (r, g, b, k for actual colours, and
|
||||
# a, m, x for alpha, map-index, and unused)
|
||||
colr = re.findall(b"[a-z]", chan)
|
||||
|
||||
depth = sum(bits)
|
||||
|
||||
# Select a "packer" that either:
|
||||
# - gathers multiple bytes into a single pixel (for depth >= 8); or,
|
||||
# - splits bytes into several pixels (for depth < 8).
|
||||
if depth >= 8:
|
||||
assert depth % 8 == 0
|
||||
packer = deblock
|
||||
else:
|
||||
assert 8 % depth == 0
|
||||
packer = bitfunge
|
||||
|
||||
for x in packer(f, depth, width):
|
||||
# x is the pixel as an unsigned integer
|
||||
o = []
|
||||
# This is a bit yucky.
|
||||
# Extract each channel from the _most_ significant part of x.
|
||||
for b, col in zip(bits, colr):
|
||||
v = (x >> (depth - b)) & mask(b)
|
||||
x <<= b
|
||||
if col != "x":
|
||||
# scale to maxval
|
||||
v = v * float(maxval) / mask(b)
|
||||
v = int(v + 0.5)
|
||||
o.append(v)
|
||||
yield o
|
||||
|
||||
|
||||
def decompress(f):
|
||||
"""Decompress a Plan 9 image file.
|
||||
The input `f` should be a binary file object that
|
||||
is already cued past the initial 'compressed\n' string.
|
||||
The return result is (`info`, `blocks`);
|
||||
`info` is a 5-tuple of the Plan 9 image metadata;
|
||||
`blocks` is an iterator that yields a (row, data) pair
|
||||
for each block of data.
|
||||
"""
|
||||
|
||||
r = meta(f.read(60))
|
||||
return r, decomprest(f, r[4])
|
||||
|
||||
|
||||
def decomprest(f, rows):
|
||||
"""Iterator that decompresses the rest of a file once the metadata
|
||||
have been consumed."""
|
||||
|
||||
row = 0
|
||||
while row < rows:
|
||||
row, o = deblock(f)
|
||||
yield o
|
||||
|
||||
|
||||
def deblock(f):
|
||||
"""Decompress a single block from a compressed Plan 9 image file.
|
||||
Each block starts with 2 decimal strings of 12 bytes each.
|
||||
Yields a sequence of (row, data) pairs where
|
||||
`row` is the total number of rows processed
|
||||
(according to the file format) and
|
||||
`data` is the decompressed data for this block.
|
||||
"""
|
||||
|
||||
row = int(f.read(12))
|
||||
size = int(f.read(12))
|
||||
if not (0 <= size <= 6000):
|
||||
raise Error("block has invalid size; not a Plan 9 image file?")
|
||||
|
||||
# Since each block is at most 6000 bytes we may as well read it all in
|
||||
# one go.
|
||||
d = f.read(size)
|
||||
i = 0
|
||||
o = []
|
||||
|
||||
while i < size:
|
||||
x = d[i]
|
||||
i += 1
|
||||
if x & 0x80:
|
||||
x = (x & 0x7F) + 1
|
||||
lit = d[i : i + x]
|
||||
i += x
|
||||
o.extend(lit)
|
||||
continue
|
||||
# x's high-order bit is 0
|
||||
length = (x >> 2) + 3
|
||||
# Offset is made from bottom 2 bits of x and 8 bits of next byte.
|
||||
# MSByte LSByte
|
||||
# +---------------------+-------------------------+
|
||||
# | - - - - - - | x1 x0 | d7 d6 d5 d4 d3 d2 d1 d0 |
|
||||
# +-----------------------------------------------+
|
||||
# Had to discover by inspection which way round the bits go,
|
||||
# because https://plan9.io/magic/man2html/6/image doesn't say.
|
||||
# that x's 2 bits are most significant.
|
||||
offset = (x & 3) << 8
|
||||
offset |= d[i]
|
||||
i += 1
|
||||
# Note: complement operator neatly maps (0 to 1023) to (-1 to
|
||||
# -1024). Adding len(o) gives a (non-negative) offset into o from
|
||||
# which to start indexing.
|
||||
offset = ~offset + len(o)
|
||||
if offset < 0:
|
||||
raise Error(
|
||||
"byte offset indexes off the begininning of "
|
||||
"the output buffer; not a Plan 9 image file?"
|
||||
)
|
||||
for j in range(length):
|
||||
o.append(o[offset + j])
|
||||
return row, bytes(o)
|
||||
|
||||
|
||||
FontChar = collections.namedtuple("FontChar", "x top bottom left width")
|
||||
|
||||
|
||||
def font_copy(inp, image, out, control):
|
||||
"""
|
||||
Convert a Plan 9 font (`inp`, `image`) to a series of PNG images,
|
||||
and write them out as a tar file to the file object `out`.
|
||||
Write a text control file out to the file object `control`.
|
||||
|
||||
Each valid glyph in the font becomes a single PNG image;
|
||||
the output is a tar file of all the images.
|
||||
|
||||
A Plan 9 font consists of a Plan 9 image immediately
|
||||
followed by font data.
|
||||
The image for the font should be the `image` argument,
|
||||
the file containing the rest of the font data should be the
|
||||
file object `inp` which should be cued up to the start of
|
||||
the font data that immediately follows the image.
|
||||
|
||||
https://plan9.io/magic/man2html/6/font
|
||||
"""
|
||||
|
||||
# The format is a little unusual, and isn't completely
|
||||
# clearly documented.
|
||||
# Each 6-byte structure (see FontChar above) defines
|
||||
# a rectangular region of the image that is used for each
|
||||
# glyph.
|
||||
# The source image region that is used may be strictly
|
||||
# smaller than the rectangle for the target glyph.
|
||||
# This seems like a micro-optimisation.
|
||||
# For each glyph,
|
||||
# rows above `top` and below `bottom` will not be copied
|
||||
# from the source (they can be assumed to be blank).
|
||||
# No space is saved in the source image, since the rows must
|
||||
# be present.
|
||||
# `x` is always non-decreasing, so the glyphs appear strictly
|
||||
# left-to-image in the source image.
|
||||
# The x of the next glyph is used to
|
||||
# infer the width of the source rectangle.
|
||||
# `top` and `bottom` give the y-coordinate of the top- and
|
||||
# bottom- sides of the rectangle in both source and targets.
|
||||
# `left` is the x-coordinate of the left-side of the
|
||||
# rectangle in the target glyph. (equivalently, the amount
|
||||
# of padding that should be added on the left).
|
||||
# `width` is the advance-width of the glyph; by convention
|
||||
# it is 0 for an undefined glyph.
|
||||
|
||||
name = getattr(inp, "name", "*subfont*name*not*supplied*")
|
||||
|
||||
header = inp.read(36)
|
||||
n, height, ascent = [int(x) for x in header.split()]
|
||||
print("baseline", name, ascent, file=control, sep=",")
|
||||
|
||||
chs = []
|
||||
for i in range(n + 1):
|
||||
bs = inp.read(6)
|
||||
ch = FontChar(*struct.unpack("<HBBBB", bs))
|
||||
chs.append(ch)
|
||||
|
||||
tar = tarfile.open(mode="w|", fileobj=out)
|
||||
|
||||
# Start at 0, increment for every image output
|
||||
# (recall that not every input glyph has an output image)
|
||||
output_index = 0
|
||||
for i in range(n):
|
||||
ch = chs[i]
|
||||
if ch.width == 0:
|
||||
continue
|
||||
|
||||
print("png", "index", output_index, "glyph", name, i, file=control, sep=",")
|
||||
|
||||
info = dict(image.info, size=(ch.width, height))
|
||||
target = new_image(info)
|
||||
|
||||
source_width = chs[i + 1].x - ch.x
|
||||
rect = ((ch.left, ch.top), (ch.left + source_width, ch.bottom))
|
||||
image_draw(target, rect, image, (ch.x, ch.top))
|
||||
|
||||
# :todo: add source, glyph, and baseline data here (as a
|
||||
# private tag?)
|
||||
o = io.BytesIO()
|
||||
target.write(o)
|
||||
binary_size = o.tell()
|
||||
o.seek(0)
|
||||
|
||||
tarinfo = tar.gettarinfo(arcname="%s/glyph%d.png" % (name, i), fileobj=inp)
|
||||
tarinfo.size = binary_size
|
||||
tar.addfile(tarinfo, fileobj=o)
|
||||
|
||||
output_index += 1
|
||||
|
||||
tar.close()
|
||||
|
||||
|
||||
def new_image(info):
|
||||
"""Return a fresh png.Image instance."""
|
||||
|
||||
width, height = info["size"]
|
||||
vpr = width * info["planes"]
|
||||
row = lambda: [0] * vpr
|
||||
rows = [row() for _ in range(height)]
|
||||
return png.Image(rows, info)
|
||||
|
||||
|
||||
def image_draw(target, rect, source, point):
|
||||
"""The point `point` in the source image is aligned with the
|
||||
top-left of rect in the target image, and then the rectangle
|
||||
in target is replaced with the pixels from `source`.
|
||||
|
||||
This routine assumes that both source and target can have
|
||||
their rows objects indexed (not streamed).
|
||||
"""
|
||||
|
||||
# :todo: there is no attempt to do clipping or channel or
|
||||
# colour conversion. But maybe later?
|
||||
|
||||
if target.info["planes"] != source.info["planes"]:
|
||||
raise NotImplementedError(
|
||||
"source and target must have the same number of planes"
|
||||
)
|
||||
|
||||
if target.info["bitdepth"] != source.info["bitdepth"]:
|
||||
raise NotImplementedError("source and target must have the same bitdepth")
|
||||
|
||||
tl, br = rect
|
||||
left, top = tl
|
||||
right, bottom = br
|
||||
height = bottom - top
|
||||
|
||||
planes = source.info["planes"]
|
||||
|
||||
vpr = (right - left) * planes
|
||||
source_left, source_top = point
|
||||
|
||||
source_l = source_left * planes
|
||||
source_r = source_l + vpr
|
||||
|
||||
target_l = left * planes
|
||||
target_r = target_l + vpr
|
||||
|
||||
for y in range(height):
|
||||
row = source.rows[y + source_top]
|
||||
row = row[source_l:source_r]
|
||||
target.rows[top + y][target_l:target_r] = row
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Convert Plan9 image to PNG")
|
||||
parser.add_argument(
|
||||
"input",
|
||||
nargs="?",
|
||||
default="-",
|
||||
type=png.cli_open,
|
||||
metavar="image",
|
||||
help="image file in Plan 9 format",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--control",
|
||||
default=os.path.devnull,
|
||||
type=argparse.FileType("w"),
|
||||
metavar="ControlCSV",
|
||||
help="(when using --font) write a control CSV file to named file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--font",
|
||||
action="store_true",
|
||||
help="process as Plan 9 subfont: output a tar file of PNGs",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
image = plan9_as_image(args.input)
|
||||
image.stream()
|
||||
|
||||
if not args.font:
|
||||
image.write(png.binary_stdout())
|
||||
else:
|
||||
font_copy(args.input, image, png.binary_stdout(), args.control)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user