Compare commits

...

5 Commits

10 changed files with 121 additions and 29 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
__pycache__
*.jpg
*.png

View File

@ -17,5 +17,8 @@
"margins": [20, 20, 20, 20],
"arrowMargin": 4,
"valuesGap": 5,
"arrowLabelDistance": 5
"arrowLabelDistance": 5,
"forceDescsOnSide": false,
"width": 1200,
"height": 800
}

View File

@ -1,6 +1,7 @@
import json
import re
class Config:
DEFAULT_FONT_FAMILY = "Ubuntu Mono"
DEFAULT_FONT_SIZE = 16
@ -21,6 +22,10 @@ class Config:
ARROW_MARGIN = 4
VALUES_GAP = 5
ARROW_LABEL_DISTANCE = 5
FORCE_DESCS_ON_SIDE = False
LEFT_LABELS = False
WIDTH = 1200
HEIGHT = 800
def __init__(self, path: str = "config.json") -> None:
self.load(path)

42
main.py
View File

@ -3,6 +3,7 @@ import os
from schema import InstructionSetSchema
description = """Examples:
- Default theme (black on white):
python main.py schema.xml -o out.jpg
@ -15,20 +16,51 @@ description = """Examples:
- Transparent background:
python main.py schema.xml -o out.png -c transparent.json
- Directory mode:
python main.py -d -o images/ schemas/
"""
def processFile(inPath, outPath, confPath, display):
schema = InstructionSetSchema(inPath, confPath, display)
schema.save(outPath)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=description, formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument("schema", help="Path to the schema description. Accepted formats are: YAML, JSON and XML")
parser.add_argument("-o", "--output", help="Output path. By default, the output file will have the same name as the schema description with the extension .png")
parser.add_argument("-c", "--config", help="Path to the config file", default="config.json")
parser.add_argument("-D", "--display", help="Enable pygame display of the result", action="store_true")
parser.add_argument("-d", "--directory", help="Enable directory mode. If set, the input and output paths are directories and all files inside the input directory are processed", action="store_true")
args = parser.parse_args()
output = args.output
if output is None:
output = os.path.splitext(args.schema)[0] + ".png"
if args.directory:
if not os.path.isdir(args.schema):
print(f"{args.schema} is not a directory")
exit(-1)
schema = InstructionSetSchema(args.schema, args.config, args.display)
schema.save(output)
output = args.output
if output is None:
output = args.schema
if not os.path.isdir(output):
print(f"{output} is not a directory")
exit(-1)
paths = os.listdir(args.schema)
for path in paths:
inPath = os.path.join(args.schema, path)
outPath = os.path.join(output, path)
outPath = os.path.splitext(outPath)[0] + ".png"
processFile(inPath, outPath, args.config, args.display)
else:
output = args.output
if output is None:
output = os.path.splitext(args.schema)[0] + ".png"
processFile(args.schema, output, args.config, args.display)

View File

@ -1,6 +1,8 @@
from __future__ import annotations
from typing import Union
class Range:
def __init__(self,
start: int,
@ -16,6 +18,7 @@ class Range:
self.description = description
self.values = values
self.dependsOn = dependsOn
self.lastValueY = -1
@property
def bits(self) -> int:

View File

@ -1,28 +1,28 @@
from __future__ import annotations
import os
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from config import Config
from range import Range
from schema import InstructionSetSchema
import pygame
from structure import Structure
from vec import Vec
class Renderer:
WIDTH = 1200
HEIGHT = 800
if TYPE_CHECKING:
from config import Config
from range import Range
from schema import InstructionSetSchema
class Renderer:
def __init__(self, config: Config, display: bool = False) -> None:
self.config = config
self.display = display
pygame.init()
if self.display:
self.win = pygame.display.set_mode([Renderer.WIDTH, Renderer.HEIGHT])
self.win = pygame.display.set_mode([self.config.WIDTH, self.config.HEIGHT])
self.surf = pygame.Surface([Renderer.WIDTH, Renderer.HEIGHT], pygame.SRCALPHA)
self.surf = pygame.Surface([self.config.WIDTH, self.config.HEIGHT], pygame.SRCALPHA)
self.font = pygame.font.SysFont(self.config.DEFAULT_FONT_FAMILY, self.config.DEFAULT_FONT_SIZE)
self.italicFont = pygame.font.SysFont(self.config.ITALIC_FONT_FAMILY, self.config.ITALIC_FONT_SIZE, italic=True)
@ -31,7 +31,12 @@ class Renderer:
def render(self, schema: InstructionSetSchema) -> None:
self.surf.fill(self.config.BACKGROUND_COLOR)
self.drawStructure(schema.structures["main"], schema.structures, self.margins[3], self.margins[0])
mainStruct: Structure = schema.structures["main"]
ox = self.margins[3]
if self.config.LEFT_LABELS:
ox = self.config.WIDTH - self.margins[3] - mainStruct.bits * self.config.BIT_WIDTH
self.drawStructure(schema.structures["main"], schema.structures, ox, self.margins[0])
if self.display:
name = os.path.basename(schema.path)
@ -45,8 +50,7 @@ class Renderer:
def drawStructure(self,
struct: Structure,
structures: dict[str,
Structure],
structures: dict[str, Structure],
ox: float = 0,
oy: float = 0) -> float:
@ -77,7 +81,21 @@ class Renderer:
pygame.draw.line(self.surf, borderCol, [bitX, bitsY], [bitX, bitsY + bitH])
ranges = struct.getSortedRanges()
descX = ox + max(0, (struct.bits-12) * bitW)
if self.config.LEFT_LABELS:
ranges.reverse()
if self.config.FORCE_DESCS_ON_SIDE:
if self.config.LEFT_LABELS:
descX = self.config.WIDTH - self.margins[3] - structures["main"].bits * bitW
else:
descX = self.margins[3] + structures["main"].bits * bitW
else:
if self.config.LEFT_LABELS:
descX = ox + struct.bits * bitW
else:
descX = ox
descY = bitsY + bitH * 2
# Names + simple descriptions
@ -123,10 +141,16 @@ class Renderer:
bitH = self.config.BIT_HEIGHT
arrowMargin = self.config.ARROW_MARGIN
if endX > startX:
endX -= arrowMargin
else:
endX += arrowMargin
pygame.draw.lines(self.surf, self.config.LINK_COLOR, False, [
[startX, startY + bitH*1.5],
[startX, endY + bitH/2],
[endX - arrowMargin, endY + bitH/2]
[endX, endY + bitH/2]
])
def drawDescription(self,
@ -140,7 +164,10 @@ class Renderer:
bitW = self.config.BIT_WIDTH
bitH = self.config.BIT_HEIGHT
descX = max(descX, rStartX + rWidth/2 + bitW)
if self.config.LEFT_LABELS:
descX = min(descX, rStartX + rWidth/2 - bitW)
else:
descX = max(descX, rStartX + rWidth/2 + bitW)
self.drawUnderbracket(rStartX, rStartX + rWidth, rStartY)
@ -148,12 +175,17 @@ class Renderer:
self.drawLink(midX, rStartY, descX, descY)
descTxt = self.font.render(range_.description, True, self.config.TEXT_COLOR)
self.surf.blit(descTxt, [descX, descY + (bitH - descTxt.get_height())/2])
txtX = descX
if self.config.LEFT_LABELS:
txtX -= descTxt.get_width()
self.surf.blit(descTxt, [txtX, descY + (bitH - descTxt.get_height())/2])
descY += descTxt.get_height()
if range_.values is not None and range_.dependsOn is None:
descX, descY = self.drawValues(range_.values, descX, descY)
txtX, descY = self.drawValues(range_.values, txtX, descY)
descY += self.config.DESCRIPTION_MARGIN
@ -191,8 +223,13 @@ class Renderer:
rWidth = range_.bits * bitW
self.drawUnderbracket(rStartX, rStartX + rWidth, bitsY)
prevY = bitsY + bitH * 1.5
dependRange = struct.ranges[range_.dependsOn]
prevRangeY = bitsY + bitH * 1.5
if dependRange.lastValueY == -1:
prevDependY = bitsY + bitH * 1.5
else:
prevDependY = dependRange.lastValueY
dependStartI = struct.bits - dependRange.end - 1
dependStartX = bitsX + dependStartI * bitW
dependWidth = dependRange.bits * bitW
@ -200,7 +237,7 @@ class Renderer:
self.drawUnderbracket(dependStartX, dependStartX + dependWidth, bitsY)
for val, data in sorted(range_.values.items(), key=lambda vd: vd[0]):
self.drawArrow(dependMid, prevY, dependMid, descY - arrowMargin)
self.drawArrow(dependMid, prevDependY, dependMid, descY - arrowMargin)
valRanges = {}
for i in range(dependRange.bits):
@ -232,11 +269,13 @@ class Renderer:
self.drawArrow(x1, y, x2, y, data["description"])
self.drawArrow(rStartX + rWidth - bitW,
prevY,
prevRangeY,
rStartX + rWidth - bitW,
descY + bitH - arrowMargin)
prevY = descY + bitH*2 + arrowMargin
prevDependY = descY + bitH*2 + arrowMargin
prevRangeY =prevDependY
dependRange.lastValueY = prevDependY
descY = self.drawStructure(structures[data["structure"]], structures, rStartX, descY)
return (descX, descY)

View File

@ -1,5 +1,6 @@
import json
import os
import yaml
from config import Config
@ -7,9 +8,11 @@ from renderer import Renderer
from structure import Structure
from xml_loader import XMLLoader
class UnsupportedFormatException(Exception):
...
class InstructionSetSchema:
VALID_EXTENSIONS = ("yaml", "json", "xml")

View File

@ -1,6 +1,8 @@
from __future__ import annotations
from range import Range
class Structure:
def __init__(self,
name: str,

2
vec.py
View File

@ -1,6 +1,8 @@
from __future__ import annotations
from math import sqrt
class Vec:
def __init__(self, x: float = 0, y: float = 0) -> None:
self.x = x

View File

@ -1,7 +1,9 @@
from __future__ import annotations
from bs4 import BeautifulSoup
from typing import TYPE_CHECKING
from bs4 import BeautifulSoup
if TYPE_CHECKING:
from io import TextIOWrapper