Initial commit.

main
Graham Sutherland 2022-11-06 03:37:52 +00:00
commit 3786542d9c
12 changed files with 2363 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
local_dev/

20
LICENSE.txt Normal file
View File

@ -0,0 +1,20 @@
Copyright (c) 2022 Graham Sutherland
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

189
README.md Normal file
View File

@ -0,0 +1,189 @@
# altium.js
altium.js is a JavaScript library for parsing and rendering Altium schematic (SchDoc) files in the browser. It currently handles most features of schematic documents as of Altium Designer version 22.5.
![altium_comparison](./altium_comparison.png)
## Quick Start
To take altium.js for a test drive, open `altium_sch.html` in a browser. A demo document will be opened and displayed. Click the "Choose File" button at the top to load a SchDoc file. It will be rendered below. If you scroll past the preview, you'll also find a CSV-compatible bill of materials (BoM).
Altium SchDoc files are OLE Compound Documents. The main stream within the document is a sequence of records, each of which comprises an ASCII/UTF8 encoded set of properties. You can read more about the internal format and record types [in the python-altium documentation](https://github.com/vadmium/python-altium/blob/master/format.md).
## Console Queries
The `altium_sch.html` page creates a global variable called `altiumDocument`, which is an `AltiumDocument` object. It has the following properties:
- `objects` - an array of objects in the document, each typed to a specific class (e.g. `AltiumComponent`, `AltiumWire`, `AltiumParameter`, etc.), with each class being a subclass of `AltiumObject`. this is the main output of the parser, and is most likely where you want to start querying for things.
- `sheet` - provides convenient access to the sheet object for the document.
- `records` - an array of record entries extracted from the underlying file format. each record entry is processed into an object by the parser. records refer to each other by their indices (starting at 0, which is usually a sheet record).
- `record_object_lookup` - a mapping object from record indices to their resultant objects. you can use this to take a record index and find the object that was created from it. each `AltiumObject` has a `record_index` property that acts as the inverse mapping, and you can also access the record directly (without needing to do a secondary lookup) using the object's `source_record` property.
- `stream` - a stream object that represents the underlying bytes of the schematic data. the schematic data stream is extracted from the SchDoc file, which is an OLE compound document.
Each `AltiumObject` has a `parent_object` field and a `child_objects` field to help traverse the document hierarchy.
### Query Examples
You can run these queries in your browser console. This section should also be useful reference if you want to roll your own tooling using this library.
#### Finding components by part name:
The following query finds all components with a design item ID of "AO3401A":
```javascript
altiumDocument.objects.filter(
o => o instanceof AltiumComponent &&
o.design_item_id == "AO3401A")
```
You can also search by description, for example if you wanted to find all MOSFETs:
```javascript
altiumDocument.objects.filter(
o => o instanceof AltiumComponent &&
o.description.includes("MOSFET"))
```
#### Finding components by designator:
Designators and components are separate objects, and there is some complexity to how they work.
You can search for a designator by its text as follows:
```javascript
altiumDocument.objects.filter(
o => o instanceof AltiumDesignator && #
o.text == "U4")
```
However, if U4 is a multi-part component (e.g. an LM324 with four separate opamp parts) this query will return separate designator objects for each part, _all_ with the text "U4". This is because the suffix letters (U4A, U4B, etc.) are added by Altium at runtime based on the associated component's current part ID. A helper property, `full_designator`, is included to get around this inconvenience:
```javascript
altiumDocument.objects.filter(
o => o instanceof AltiumDesignator &&
o.full_designator == "U4A")
```
The parent object of the designator is _usually_ an `AltiumComponent`, so you can use the `parent_object` property to find parts with the exact specified designator:
```javascript
altiumDocument.objects.filter(
o => o instanceof AltiumDesignator &&
o.full_designator == "U4A"
).map(d => d.parent_object)
```
However, due to implementation complexities and variances across Altium versions, there may be additional mapping objects sat between the current object and the parent object you want to find. In such a case, the direct parent object of the `AltiumDesignator` might not be an `AltiumComponent`, but something else. To make things more reliable, you can use the `find_parent()` helper function on any `AltiumObject` in order to find a parent of a specific type:
```javascript
altiumDocument.objects.filter(
o => o instanceof AltiumDesignator &&
o.full_designator == "R3"
).map(d => d.find_parent(AltiumComponent))
```
#### Finding components by footprint:
Footprints, simulation models, and signal integrity models are all represented internally by "implementations". All implementations are saved in the file _regardless_ of whether they are the currently active ones, so if you've got a generic resistor part with footprints for 0603, 0805, etc. then there will be separate `AltiumImplementation` objects for each of those. You can tell whether an implementation is actively selected by checking its `is_current` property.
Each `AltiumImplementation` is typically a child of an `AltiumImplementationList` object, which itself is typically a child of an `AltiumComponent`. The `find_parent()` helper is useful for traversing the hierarchy here.
For example, this query finds all components that have a SOT23 footprint currently selected:
```javascript
altiumDocument.objects.filter(
o => o instanceof AltiumImplementation &&
o.is_footprint && o.is_current &&
o.model_name.includes("SOT23")
).map(f => f.find_parent(AltiumComponent))
```
#### Finding power ports:
You can search for power ports by name as follows:
```javascript
altiumDocument.objects.filter(
o => o instanceof AltiumPowerPort &&
o.text == "VCC")
```
You can also search by the _type_ of power port:
```javascript
altiumDocument.objects.filter(
o => o instanceof AltiumPowerPort &&
o.style_name == "POWER_GND")
```
The defined power port names are ARROW, BAR, WAVE, POWER_GND, SIGNAL_GND, EARTH, GOST_ARROW, GOST_POWER_GND, GOST_EARTH, and GOST_BAR. A value of DEFAULT is used to represent two possible port types: when `is_off_sheet_connector` is false it represents a circle port, and when it is true it represents an off-sheet connector.
#### Accessing underlying properties:
The parser handles a lot of the translation from underlying attributes into a clean format, but, for the most part, only the properties that were necessary to implement the renderer are actually parsed out to a high-level value. You can still access all of the underlying attributes, though, through the `attributes` and `raw_attributes` fields on every `AltiumObject`. If you can't find the value you're looking for directly in an object, check the attributes field.
The `attributes` field is an object built from all of the underlying attributes, with special characters in the name replaced with underscores. You can also access the raw attributes as an array of objects with `name` and `value` fields. All values are strings.
For example, if I wanted to know whether the sheet border is enabled, I could check the following value:
```javascript
altiumDocument.sheet.attributes.borderon
```
This will either be "T" for true, or "F" for false. It could also be missing, since many fields are not added to documents if the field value is the default.
## Rolling your own
The `altium_sch_document.js` script contains the parser, which you invoke using the `AltiumDocument` class constructor. It needs access to the data from inside the "FileHeader" stream of the OLE Compound Document file. A rudimentary OLE parser is included in `ole.js`, and several helper scripts are also required (`base64_binary.js`, `helper_extensions.js`, and `u8stream.js`).
Given a SchDoc file as an `ArrayBuffer` object, you can invoke the parser as follows:
```javascript
// parse the ArrayBuffer as an OLE Compound Document
let ole = new OLE(data);
// find the FileHeader stream
let fileHeader = ole.directory_entries.find(de => de.name == "FileHeader");
// read the result as a U8Stream (a wrapper around Uint8Array)
let fileHeaderData = fileHeader.read_all();
// parse it as an Altium document
let altiumDocument = new AltiumDocument(fileHeaderData);
```
The `altium_sch_renderer.js` script takes a document and renders it to a canvas object:
```javascript
let renderer = new AltiumSchematicRenderer(canvas, altiumDocument);
renderer.render();
```
You can take a look at `altium_sch.html` for further reference.
## Known issues
There are some known issues.
**Parser:**
- The OLE parser is very rudimentary and only supports reading data from a single contiguous stream. My understanding of OLE Compound Document formats is limited, but it is possible that very large SchDoc files (55MB+) will not load properly due to exceeding 110 sectors in size.
**Renderer:**
- Buses and bus entries aren't drawn.
- Ports (regular ones, not power ports) aren't drawn.
- "No ERC" blocks and markers aren't drawn.
- Sheet symbols and sheet entries aren't drawn.
- Sheet information blocks aren't drawn.
- Vertical alignment of text in text frames does not account for extra lines caused by word wrap.
- Alignment of rotated text is sometimes a bit off, and vertical text may be horizontally flipped in some cases compared to Altium.
- Only power bars, power ground, and earth symbols are supported for power ports. All others are just drawn as a box.
- Power port symbols are always drawn in their "natural" orientation, i.e. ground facing down and power bars facing up.
If you want to open a PR to fix any of these, that'd be appreciated.
## License
altium.js and its components are released under [MIT License](LICENSE.txt).
The `base64_binary.js` file was developed by Daniel Guerrero. See the file contents for software licensing information.
Special thanks to Martin Panter, who authored the [python-altium library](https://github.com/vadmium/python-altium) and saved me a lot of reverse engineering work.

BIN
altium_comparison.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

50
altium_sch.html Normal file
View File

@ -0,0 +1,50 @@
<html>
<head>
<script type="text/javascript" src="base64_binary.js"></script>
<script type="text/javascript" src="helper_extensions.js"></script>
<script type="text/javascript" src="u8stream.js"></script>
<script type="text/javascript" src="ole.js"></script>
<script type="text/javascript" src="altium_sch_document.js"></script>
<script type="text/javascript" src="altium_sch_renderer.js"></script>
<script type="text/javascript" src="test_schdoc.js"></script>
</head>
<body>
<div><input type="file" id="altium-file"></input></div>
<div><canvas id="canvas" width="1024" height="768"></canvas></div>
<div><pre id="results"></pre></div>
<script type="text/javascript">
renderSchematic(getTestFile());
function readSchematicFile(e)
{
let file = e.target.files[0];
if (!file)
{
return;
}
let reader = new FileReader();
reader.onload = function(e)
{
let contents = e.target.result;
renderSchematic(contents);
};
reader.readAsArrayBuffer(file);
}
document.getElementById('altium-file').addEventListener('change', readSchematicFile, false);
function renderSchematic(data)
{
let canvas = document.getElementById("canvas");
let ole = new OLE(data);
let fhEntry = ole.directory_entries.find((de) => de.name == "FileHeader");
let fhData = fhEntry.read_all();
let altiumDocument = new AltiumDocument(fhData);
window.altiumDocument = altiumDocument;
let renderer = new AltiumSchematicRenderer(canvas, altiumDocument);
renderer.render();
}
</script>
</body>
</html>

785
altium_sch_document.js Normal file
View File

@ -0,0 +1,785 @@
/*
altium.js schematic document parser
Copyright (c) 2022 Graham Sutherland
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
class AltiumRecord
{
static #stringDecoder = new TextDecoder('utf-8');
static get StringDecoder() { return AltiumRecord.#stringDecoder; }
get string_contents()
{
return AltiumRecord.StringDecoder.decode(this.data).slice(0, -1);
}
get attributes()
{
const regex = /(?:\|(?<name>[^|=]+?)=(?<value>[^|=]+))/gm;
let contents = this.string_contents;
return Array.from(contents.matchAll(regex), (m) => m.groups);
}
constructor(stream, index)
{
this.record_index = index;
this.stream = stream;
this.position = stream.u8stream_position;
this.payload_length = stream.read_u16_le();
this.padding = stream.read_u8();
if (this.padding != 0)
console.warn("Padding byte on record index " + index.toString() + " was non-zero.");
this.record_type = stream.read_u8();
if (this.record_type != 0)
throw new Error("Invalid record type.");
this.data = stream.read(this.payload_length);
this.record_id = -1;
if (this.data.length > "|RECORD=255|".length)
{
// check if this starts with |RECORD=
if (this.data.compare_to(new Uint8Array([0x7c, 0x52, 0x45, 0x43, 0x4f, 0x52, 0x44, 0x3d])))
{
let recordFieldStr = AltiumRecord.StringDecoder.decode(this.data.slice(8, 12));
let recordIdStr = recordFieldStr.split('|')[0];
this.record_id = Number.parseInt(recordIdStr, 10);
}
}
}
}
class AltiumObject
{
static RecordObjectMap = [];
constructor(record)
{
this.record_index = record.record_index;
this.source_record = record;
this.attributes_raw = record.attributes;
this.attributes = {};
for (let attrib of this.attributes_raw)
{
this.attributes[attrib.name.toLowerCase().replaceAll('%', '_').replace('.', '_')] = attrib.value;
}
this.owner_record_index = Number.parseInt(this.attributes.ownerindex ?? "-1", 10);
this.index_in_sheet = Number.parseInt(this.attributes.indexinsheet ?? "-1", 10);
this.owner_part_id = (this.attributes.ownerpartid == null) ? null : Number.parseInt(this.attributes.ownerpartid, 10);
this.parent_object = null;
this.child_objects = [];
}
find_parent(type)
{
let currentParent = this.parent_object;
const BLOWN = 1024; // nesting limit
let fuse = 0;
while ((++fuse != BLOWN) && (currentParent != null) && !(currentParent instanceof type))
{
currentParent = currentParent.parent_object;
}
if (fuse >= BLOWN)
return null;
return currentParent;
}
}
class AltiumComponent extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 1, name: "Component", type: this }) }
constructor(record)
{
super(record);
this.library_reference = this.attributes.libreference;
this.design_item_id = this.attributes.designitemid;
this.description = (this.attributes._utf8_componentdescription ?? this.attributes.componentdescription) ?? "";
this.current_part_id = Number.parseInt(this.attributes.currentpartid ?? "-1", 10);
this.part_count = Number.parseInt(this.attributes.partcount ?? "1", 10);
}
}
class AltiumPin extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 2, name: "Pin", type: this }) }
constructor(record)
{
super(record);
this.x = Number.parseInt(this.attributes.location_x, 10);
this.y = Number.parseInt(this.attributes.location_y, 10);
this.length = Number.parseInt(this.attributes.pinlength, 10);
let conglomerate = Number.parseInt(this.attributes.pinconglomerate, 10);
this.orientation = conglomerate & 3;
this.angle = 90 * this.orientation;
this.name = (this.attributes._utf8_name ?? this.attributes.name) ?? "";
this.show_name = (conglomerate & 0x8) == 0x8;
this.show_designator = (conglomerate & 0x10) == 0x10;
const angle_vec_table = [
[1, 0],
[0, 1],
[-1, 0],
[0, -1]
];
this.angle_vec = angle_vec_table[this.orientation];
// unclear values here. python-altium docs suggest values of 0,16,21, but in practice I've only seen 5.
this.name_orientation = Number.parseInt(this.attributes.pinname_positionconglomerate ?? "0", 10);
}
}
class AltiumIEEESymbol extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 3, name: "IEEE Symbol", type: this }) }
constructor(record)
{
super(record);
}
}
class AltiumLabel extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 4, name: "Label", type: this }) }
constructor(record)
{
super(record);
this.text = this.attributes.text;
this.hidden = (this.attributes.ishidden ?? "") == "T";
this.colour = Number.parseInt(this.attributes.color ?? "0", 10);
this.x = Number.parseInt(this.attributes.location_x, 10);
this.y = Number.parseInt(this.attributes.location_y, 10);
this.orientation = Number.parseInt(this.attributes.orientation ?? "0", 10);
this.justification = Number.parseInt(this.attributes.justification ?? "0", 10);
}
}
class AltiumBezier extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 5, name: "Bezier", type: this }) }
constructor(record)
{
super(record);
}
}
class AltiumPolyline extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 6, name: "Polyline", type: this }) }
constructor(record)
{
super(record);
this.points = [];
let idx = 1;
while (this.attributes["x" + idx.toString()] != null)
{
let x = Number.parseInt(this.attributes["x" + idx.toString()], 10);
let y = Number.parseInt(this.attributes["y" + idx.toString()], 10);
this.points.push({ x: x, y: y });
idx++;
}
this.width = Number.parseInt(this.attributes.linewidth ?? "0", 10);
this.colour = Number.parseInt(this.attributes.color ?? "0", 10);
this.start_shape = Number.parseInt(this.attributes.startlineshape ?? "0", 10);
this.end_shape = Number.parseInt(this.attributes.endlineshape ?? "0", 10);
this.shape_size = Number.parseInt(this.attributes.lineshapesize ?? "0", 10);
this.line_style = Number.parseInt(this.attributes.linestyle ?? "0", 10); // 0 = solid, 1 = dashed, 2 = dotted, 3 = dash-dotted
}
}
class AltiumPolygon extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 7, name: "Polygon", type: this }) }
constructor(record)
{
super(record);
this.points = [];
let idx = 1;
while (this.attributes["x" + idx.toString()] != null)
{
let x = Number.parseInt(this.attributes["x" + idx.toString()], 10);
let y = Number.parseInt(this.attributes["y" + idx.toString()], 10);
this.points.push({ x: x, y: y });
idx++;
}
this.width = Number.parseInt(this.attributes.linewidth ?? "0", 10);
this.line_colour = Number.parseInt(this.attributes.color ?? "0", 10);
this.fill_colour = Number.parseInt(this.attributes.areacolor ?? "0", 10);
}
}
class AltiumEllipse extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 8, name: "Ellipse", type: this }) }
constructor(record)
{
super(record);
this.x = Number.parseInt(this.attributes.location_x, 10);
this.y = Number.parseInt(this.attributes.location_y, 10);
this.radius_x = Number.parseInt(this.attributes.radius, 10);
if (this.attributes.secondaryradius != null)
this.radius_y = Number.parseInt(this.attributes.secondaryradius, 10);
else
this.radius_y = this.radius_x;
this.width = Number.parseInt(this.attributes.linewidth ?? "1", 10);
this.line_colour = Number.parseInt(this.attributes.color ?? "0", 10);
this.fill_colour = Number.parseInt(this.attributes.areacolor ?? "0", 10);
this.transparent = (this.attributes.issolid ?? "") != "T";
}
}
class AltiumPiechart extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 9, name: "Piechart", type: this }) }
constructor(record)
{
super(record);
}
}
class AltiumRoundedRectangle extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 10, name: "Rounded Rectangle", type: this }) }
constructor(record)
{
super(record);
}
}
class AltiumEllipticalArc extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 11, name: "Ellipitcal Arc", type: this }) }
constructor(record)
{
super(record);
}
}
class AltiumArc extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 12, name: "Arc", type: this }) }
constructor(record)
{
super(record);
this.x = Number.parseInt(this.attributes.location_x, 10);
this.y = Number.parseInt(this.attributes.location_y, 10);
this.radius = Number.parseInt(this.attributes.radius, 10);
this.start_angle = Number.parseFloat(this.attributes.startangle ?? "0");
this.end_angle = Number.parseFloat(this.attributes.endangle ?? "360");
this.width = Number.parseInt(this.attributes.linewidth ?? "1", 10);
this.colour = Number.parseInt(this.attributes.color ?? "0", 10);
}
}
class AltiumLine extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 13, name: "Line", type: this }) }
constructor(record)
{
super(record);
this.x1 = Number.parseInt(this.attributes.location_x, 10);
this.y1 = Number.parseInt(this.attributes.corner_x, 10);
this.x2 = Number.parseInt(this.attributes.corner_y, 10);
this.y2 = Number.parseInt(this.attributes.location_y, 10);
this.width = Number.parseInt(this.attributes.linewidth ?? "1", 10);
this.colour = Number.parseInt(this.attributes.color ?? "0", 10);
}
}
class AltiumRectangle extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 14, name: "Rectangle", type: this }) }
constructor(record)
{
super(record);
this.left = Number.parseInt(this.attributes.location_x, 10);
this.right = Number.parseInt(this.attributes.corner_x, 10);
this.top = Number.parseInt(this.attributes.corner_y, 10);
this.bottom = Number.parseInt(this.attributes.location_y, 10);
this.line_colour = Number.parseInt(this.attributes.color, 10);
this.fill_colour = Number.parseInt(this.attributes.areacolor, 10);
this.transparent = (this.attributes.issolid ?? "F") != "T" || (this.attributes.transparent ?? "F") == "T";
}
}
class AltiumSheetSymbol extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 15, name: "Sheet Symbol", type: this }) }
constructor(record)
{
super(record);
}
}
class AltiumSheetEntry extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 16, name: "Sheet Entry", type: this }) }
constructor(record)
{
super(record);
}
}
class AltiumPowerPort extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 17, name: "Power Port", type: this }) }
constructor(record)
{
super(record);
this.x = Number.parseInt(this.attributes.location_x, 10);
this.y = Number.parseInt(this.attributes.location_y, 10);
this.colour = Number.parseInt(this.attributes.color ?? "0", 10);
this.show_text = (this.attributes.shownetname ?? "") == "T";
this.text = (this.attributes._utf8_text ?? this.attributes.text) ?? "";
this.style = Number.parseInt(this.attributes.style ?? "0", 10);
const styleNames = ["DEFAULT", "ARROW", "BAR", "WAVE", "POWER_GND", "SIGNAL_GND", "EARTH", "GOST_ARROW", "GOST_POWER_GND", "GOST_EARTH", "GOST_BAR"];
this.style_name = (this.style < 0 || this.style > styleNames.length-1) ? "UNKNOWN" : styleNames[this.style];
this.orientation = Number.parseInt(this.attributes.orientation ?? "0", 10);
this.is_off_sheet_connector = (this.attributes.iscrosssheetconnector ?? "") == "T";
}
}
class AltiumPort extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 18, name: "Port", type: this }) }
constructor(record)
{
super(record);
}
}
class AltiumNoERC extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 22, name: "No ERC", type: this }) }
constructor(record)
{
super(record);
}
}
class AltiumNetLabel extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 25, name: "Net Label", type: this }) }
constructor(record)
{
super(record);
this.x = Number.parseInt(this.attributes.location_x, 10);
this.y = Number.parseInt(this.attributes.location_y, 10);
this.colour = Number.parseInt(this.attributes.color ?? "0", 10);
this.text = (this.attributes._utf8_text ?? this.attributes.text) ?? "";
this.orientation = Number.parseInt(this.attributes.orientation ?? "0", 10);
this.justification = Number.parseInt(this.attributes.justification ?? "0", 10);
}
}
class AltiumBus extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 26, name: "Bus", type: this }) }
constructor(record)
{
super(record);
}
}
class AltiumWire extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 27, name: "Wire", type: this }) }
constructor(record)
{
super(record);
this.points = [];
let idx = 1;
while (this.attributes["x" + idx.toString()] != null)
{
let x = Number.parseInt(this.attributes["x" + idx.toString()], 10);
let y = Number.parseInt(this.attributes["y" + idx.toString()], 10);
this.points.push({ x: x, y: y });
idx++;
}
this.colour = Number.parseInt(this.attributes.color, 10);
}
}
class AltiumTextFrame extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 28, name: "Text Frame", type: this }) }
constructor(record)
{
super(record);
this.left = Number.parseInt(this.attributes.location_x, 10);
this.bottom = Number.parseInt(this.attributes.location_y, 10);
this.right = Number.parseInt(this.attributes.corner_x, 10);
this.top = Number.parseInt(this.attributes.corner_y, 10);
this.border_colour = Number.parseInt(this.attributes.color ?? "0", 10);
this.text_colour = Number.parseInt(this.attributes.textcolor ?? "0", 10);
this.fill_colour = Number.parseInt(this.attributes.areacolor ?? "16777215", 10);
this.text = (this.attributes._utf8_text ?? this.attributes.text) ?? "";
this.orientation = Number.parseInt(this.attributes.orientation ?? "0", 10);
this.alignment = Number.parseInt(this.attributes.alignment ?? "0", 10);
this.show_border = (this.attributes.showborder ?? "") == "T";
this.transparent = (this.attributes.issolid ?? "") != "F";
this.text_margin = Number.parseInt(this.attributes.textmargin ?? "2", 10);
this.word_wrap = (this.attributes.wordwrap ?? "F") == "T";
this.font_id = Number.parseInt(this.attributes.fontid ?? "-1", 10);
}
}
class AltiumJunction extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 29, name: "Junction", type: this }) }
constructor(record)
{
super(record);
this.x = Number.parseInt(this.attributes.location_x, 10);
this.y = Number.parseInt(this.attributes.location_y, 10);
this.colour = Number.parseInt(this.attributes.color, 10);
}
}
class AltiumImage extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 30, name: "Image", type: this }) }
constructor(record)
{
super(record);
}
}
class AltiumSheet extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 31, name: "Sheet", type: this }) }
static #sheetSizes = [
[1150, 760],
[1550, 1110],
[2230, 1570],
[3150, 2230],
[4460, 3150],
[950, 750],
[1500, 950],
[2000, 1500],
[3200, 2000],
[4200, 3200],
[1100, 850],
[1400, 850],
[1700, 1100],
[990, 790],
[1540, 990],
[2060, 1560],
[3260, 2060],
[4280, 3280]
];
constructor(record)
{
super(record);
this.grid_size = Number.parseInt(this.attributes.visiblegridsize ?? "10", 10);
this.show_grid = (this.attributes.visiblegridon ?? "") != "F";
if (this.attributes.usecustomsheet == 'T')
{
this.width = Number.parseInt(this.attributes.customx, 10);
this.height = Number.parseInt(this.attributes.customy, 10);
}
else
{
let paperSize = Number.parseInt(this.attributes.sheetstyle ?? "0", 10);
if (paperSize < AltiumSheet.#sheetSizes.length)
{
this.width = AltiumSheet.#sheetSizes[paperSize][0];
this.height = AltiumSheet.#sheetSizes[paperSize][1];
}
}
let f = 1;
this.fonts = {};
while (this.attributes["fontname" + f.toString()] != null)
{
const fontName = this.attributes["fontname" + f.toString()];
const fontSize = Number.parseInt(this.attributes["size" + f.toString()] ?? "12", 10);
this.fonts[f] = { name: fontName, size: fontSize };
f++;
}
}
}
class AltiumDesignator extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 34, name: "Designator", type: this }) }
get full_designator()
{
const parent = this.find_parent(AltiumComponent);
if (parent == null)
return this.text;
if (parent.part_count <= 2) // for some reason part count is 2 for single-part components
return this.text;
if (parent.current_part_id <= 0)
return this.text;
if (parent.current_part_id <= 26)
return this.text + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[parent.current_part_id-1];
else
return this.text + "[" + parent.current_part_id + "]";
}
constructor(record)
{
super(record);
this.x = Number.parseInt(this.attributes.location_x ?? "0", 10);
this.y = Number.parseInt(this.attributes.location_y ?? "0", 10);
this.colour = Number.parseInt(this.attributes.colour ?? "0", 10);
this.hidden = (this.attributes.ishidden ?? "") == "T";
this.text = (this.attributes._utf8_text ?? this.attributes.text) ?? "";
this.mirrored = (this.attributes.ismirrored ?? "") == "T";
this.orientation = Number.parseInt(this.attributes.orientation ?? "0", 10);
}
}
class AltiumParameter extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 41, name: "Parameter", type: this }) }
get is_implementation_parameter()
{
return this.parent_object instanceof AltiumImplementationParameterList;
}
constructor(record)
{
super(record);
this.x = Number.parseInt(this.attributes.location_x ?? "0", 10);
this.y = Number.parseInt(this.attributes.location_y ?? "0", 10);
this.colour = Number.parseInt(this.attributes.colour ?? "0", 10);
this.text = (this.attributes._utf8_text ?? this.attributes.text) ?? "";
this.hidden = (this.attributes.ishidden ?? "") == "T";
this.mirrored = (this.attributes.ismirrored ?? "") == "T";
this.orientation = Number.parseInt(this.attributes.orientation ?? "0", 10);
}
}
class AltiumWarningSign extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 43, name: "Warning Sign", type: this }) }
constructor(record)
{
super(record);
}
}
class AltiumImplementationList extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 44, name: "Implementation List", type: this }) }
constructor(record)
{
super(record);
}
}
class AltiumImplementation extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 45, name: "Implementation", type: this }) }
constructor(record)
{
super(record);
this.is_current = (this.attributes.iscurrent ?? "") == "T";
this.description = this.attributes.description;
this.model_name = this.attributes.modelname;
this.is_footprint = this.attributes.modeltype == "PCBLIB";
this.is_sim = this.attributes.modeltype == "SIM";
this.is_signal_integrity = this.attributes.modeltype == "SI";
}
}
class AltiumImplementationPinAssociation extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 46, name: "Implementation Pin Association", type: this }) }
constructor(record)
{
super(record);
}
}
class AltiumImplementationPin extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 47, name: "Implementation Pin", type: this }) }
constructor(record)
{
super(record);
this.pin_name = this.attributes.desintf;
}
}
class AltiumImplementationParameterList extends AltiumObject
{
static { AltiumObject.RecordObjectMap.push({ id: 48, name: "Implementation Parameter List", type: this }) }
constructor(record)
{
super(record);
}
}
class AltiumDocument
{
constructor(stream)
{
this.stream = stream;
this.records = [];
let index = -1; // header comes first, so give it an index of -1
while (this.stream.u8stream_position < this.stream.length)
{
this.records.push(new AltiumRecord(this.stream, index));
index++;
}
this.objects = [];
let record_object_lookup = {};
for (let record of this.records)
{
// skip the header object
if (record.record_index < 0)
continue;
let mapping = AltiumObject.RecordObjectMap.find((rom) => rom.id == record.record_id);
let recordObject = null;
if (mapping != null)
{
const objectType = mapping.type;
recordObject = new objectType(record);
}
else
{
// generic object (specific parsing unimplemented)
recordObject = new AltiumObject(record);
recordObject.is_unknown_type = true;
}
this.objects.push(recordObject);
record_object_lookup[record.record_index] = recordObject;
}
for (let object of this.objects)
{
if (object.owner_record_index < 0)
continue;
let ownerObject = record_object_lookup[object.owner_record_index];
if (ownerObject == null)
continue;
object.parent_object = ownerObject;
ownerObject.child_objects.push(object);
}
this.record_object_lookup = record_object_lookup;
this.sheet = this.objects.find(o => o instanceof AltiumSheet);
}
object_from_record_index(index)
{
for (let obj of this.objects)
{
if (obj.record_index == index)
return obj;
}
return null;
}
find_parent_record(start_index, record_type)
{
let currentRecord = this.records.find((r) => r.record_index == start_index);
if (currentRecord == null)
return null;
while (true)
{
if (currentRecord.record_id == record_type)
return currentRecord;
let ownerIndexAttr = currentRecord.attributes.find((a) => a.name.toLowerCase() == "ownerindex");
if (ownerIndexAttr == null || ownerIndexAttr?.value == null || ownerIndexAttr?.value == "")
return null;
let ownerIndex = Number.parseInt(ownerIndexAttr.value, 10);
if (ownerIndex < 0)
return null;
let nextRecord = this.records.find((r) => r.record_index == ownerIndex);
if (nextRecord == null)
return null;
currentRecord = nextRecord;
}
}
find_child_records(parent_index, record_type=null)
{
results = [];
for (let currentRecord in this.records)
{
if (record_type != null && currentRecord.record_id != record_type)
continue;
let ownerIndexAttr = currentRecord.attributes.find((a) => a.name.toLowerCase() == "ownerindex");
if (ownerIndexAttr == null || ownerIndexAttr?.value == null || ownerIndexAttr?.value == "")
continue;
let ownerIndex = Number.parseInt(ownerIndexAttr.value, 10);
if (ownerIndex == parent_index)
results.push(currentRecord);
}
return results;
}
}

760
altium_sch_renderer.js Normal file
View File

@ -0,0 +1,760 @@
/*
altium.js schematic renderer
Copyright (c) 2022 Graham Sutherland
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
class AltiumSchematicRenderer
{
constructor(canvas, document)
{
this.canvas = canvas;
this.document = document;
}
#altiumColourToHex(colourInt)
{
return "#" + (colourInt & 0xFF).toString(16).padStart(2, '0') + ((colourInt >> 8) & 0xFF).toString(16).padStart(2, '0') + ((colourInt >> 16) & 0xFF).toString(16).padStart(2, '0');
}
#shouldShow(object)
{
if (object.owner_part_id == null || object.owner_part_id < 1)
return true;
const parent = object.find_parent(AltiumComponent);
if (parent == null)
return true;
if (parent.current_part_id == null || parent.current_part_id < 1)
return true;
return parent.current_part_id == object.owner_part_id;
}
render()
{
let canvas = this.canvas;
let doc = this.document;
let sheetObject = doc.objects.find(o => o instanceof AltiumSheet);
canvas.style.width = sheetObject.width + "px";
canvas.style.height = sheetObject.height + "px";
canvas.width = sheetObject.width * window.devicePixelRatio;
canvas.height = sheetObject.height * window.devicePixelRatio;
let areaColourInt = Number.parseInt(sheetObject.attributes.areacolor, 10);
let areaColour = this.#altiumColourToHex(areaColourInt);
canvas.style.backgroundColor = areaColour;
let ctx = canvas.getContext('2d');
ctx.scale(1, -1);
ctx.translate(0.5, 0.5);
ctx.translate(0, -canvas.height);
ctx.font = "7pt sans-serif";
ctx.textRendering = "optimizeLegibility";
ctx.imageSmoothingQuality = "high";
ctx.textBaseline = "bottom";
ctx.fillStyle = areaColour;
ctx.fillRect(0, 0, canvas.width, canvas.height);
let results = document.getElementById("results");
let sheet = doc.objects.find((o) => o instanceof AltiumSheet);
let gridLight = "#eeeeee";
let gridDark = "#cccccc";
ctx.lineWidth = 1;
ctx.globalAlpha = 0.5;
if (sheet.show_grid)
{
let n = 0;
for (let x = 0; x < canvas.width; x += sheet.grid_size)
{
ctx.strokeStyle = ((n % 10) == 0) ? gridDark : gridLight;
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
n++;
}
n = 0;
for (let y = 0; y < canvas.height; y += sheet.grid_size)
{
ctx.strokeStyle = ((n % 10) == 0) ? gridDark : gridLight;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
n++;
}
}
ctx.globalAlpha = 1;
/*
ctx.textAlign = "center";
ctx.font = "bold 100px serif";
ctx.fillStyle = "#333300";
ctx.globalAlpha = 0.03;
ctx.save();
ctx.rotate(Math.PI/4);
ctx.scale(1,-1);
for (let y = 0; y < canvas.height * 2; y += 400)
{
ctx.fillText("PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA", canvas.width / 2, canvas.height - (y + 200));
ctx.fillText("BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW - BETA - PREVIEW", canvas.width / 2, canvas.height - y);
}
ctx.textAlign = "left";
*/
let bom = [];
bom.push("\"designator\", \"part\", \"description\"");
for (let obj of doc.objects.filter((o) => o instanceof AltiumDesignator))
{
if (!this.#shouldShow(obj)) continue;
let bomLine = "";
//let designator = doc.objects.find((des) => des instanceof AltiumDesignator && des.owner_part_id == obj.current_part_id);
let component = doc.object_from_record_index(obj.owner_record_index);
if (component != null && component instanceof AltiumComponent)
{
bomLine += "\"" + obj.text + "\", \"" + component.design_item_id + "\", \"" + component.description.replaceAll("\"", "'") + "\"";
bom.push(bomLine);
}
//bomLine += obj.description;
}
results.innerText = bom.join("\n");
for (let obj of doc.objects.filter((o) => o instanceof AltiumWire))
{
if (!this.#shouldShow(obj)) continue;
ctx.strokeStyle = this.#altiumColourToHex(obj.colour);
ctx.lineWidth = obj.width;
ctx.beginPath();
ctx.moveTo(obj.points[0].x, obj.points[0].y);
for (let i = 1; i < obj.points.length; i++)
{
ctx.lineTo(obj.points[i].x, obj.points[i].y);
}
ctx.stroke();
}
for (let obj of doc.objects.filter((o) => o instanceof AltiumRectangle))
{
if (!this.#shouldShow(obj)) continue;
if (!obj.transparent)
{
ctx.fillStyle = this.#altiumColourToHex(obj.fill_colour);
ctx.fillRect(obj.left, obj.top, obj.right - obj.left, obj.bottom - obj.top);
}
ctx.strokeStyle = this.#altiumColourToHex(obj.line_colour);
ctx.strokeRect(obj.left, obj.top, obj.right - obj.left, obj.bottom - obj.top);
}
for (let obj of doc.objects.filter((o) => o instanceof AltiumTextFrame))
{
if (!this.#shouldShow(obj)) continue;
if (!obj.transparent)
{
ctx.fillStyle = this.#altiumColourToHex(obj.fill_colour);
ctx.fillRect(obj.left, obj.top, obj.right - obj.left, obj.bottom - obj.top);
}
if (obj.show_border)
{
ctx.strokeStyle = this.#altiumColourToHex(obj.border_colour);
ctx.strokeRect(obj.left, obj.top, obj.right - obj.left, obj.bottom - obj.top);
}
}
for (let obj of doc.objects.filter((o) => o instanceof AltiumEllipse))
{
if (!this.#shouldShow(obj)) continue;
if (!obj.transparent)
{
ctx.fillStyle = this.#altiumColourToHex(obj.fill_colour);
}
ctx.strokeStyle = this.#altiumColourToHex(obj.line_colour);
ctx.beginPath();
ctx.ellipse(obj.x, obj.y, obj.radius_x, obj.radius_y, 0, 0, Math.PI*2);
ctx.stroke();
if (!obj.transparent)
ctx.fill();
}
for (let obj of doc.objects.filter((o) => o instanceof AltiumPin))
{
if (!this.#shouldShow(obj)) continue;
ctx.strokeStyle = "#000000";
ctx.beginPath();
ctx.moveTo(obj.x, obj.y);
ctx.lineTo(obj.x + obj.angle_vec[0] * obj.length, obj.y + obj.angle_vec[1] * obj.length);
ctx.stroke();
}
for (let obj of doc.objects.filter((o) => o instanceof AltiumLine))
{
if (!this.#shouldShow(obj)) continue;
ctx.strokeStyle = this.#altiumColourToHex(obj.colour);
ctx.beginPath();
ctx.moveTo(obj.x1, obj.y1);
ctx.lineTo(obj.x2, obj.y2);
ctx.stroke();
}
for (let obj of doc.objects.filter((o) => o instanceof AltiumArc))
{
if (!this.#shouldShow(obj)) continue;
ctx.strokeStyle = this.#altiumColourToHex(obj.colour);
ctx.lineWidth = obj.width;
ctx.beginPath();
ctx.arc(obj.x, obj.y, obj.radius, obj.start_angle * Math.PI/180, obj.end_angle * Math.PI/180);
ctx.stroke();
ctx.lineWidth = 1;
}
for (let obj of doc.objects.filter((o) => o instanceof AltiumPolyline))
{
if (!this.#shouldShow(obj)) continue;
ctx.strokeStyle = this.#altiumColourToHex(obj.colour);
ctx.fillStyle = this.#altiumColourToHex(obj.colour);
ctx.lineWidth = obj.width;
switch (obj.line_style)
{
case 1:
ctx.setLineDash([4, 4]);
break;
case 2:
ctx.setLineDash([2, 2]);
break;
case 3:
ctx.setLineDash([4, 2, 2, 4]);
break;
}
ctx.beginPath();
ctx.moveTo(obj.points[0].x, obj.points[0].y);
for (let i = 1; i < obj.points.length; i++)
{
ctx.lineTo(obj.points[i].x, obj.points[i].y);
}
ctx.stroke();
ctx.setLineDash([]);
let pa = null;
let pb = null;
let shapeSize = obj.shape_size + 1;
ctx.lineWidth = shapeSize;
if (obj.start_shape > 0)
{
let pa = obj.points[1];
let pb = obj.points[0];
let dx = pb.x - pa.x;
let dy = pb.y - pa.y;
let angle = Math.atan2(dy, dx);
const baseSize = 3 + shapeSize;
let tax = pb.x - Math.cos(angle - Math.PI/6) * baseSize;
let tay = pb.y - Math.sin(angle - Math.PI/6) * baseSize;
let tbx = pb.x - Math.cos(angle + Math.PI/6) * baseSize;
let tby = pb.y - Math.sin(angle + Math.PI/6) * baseSize;
ctx.beginPath();
ctx.moveTo(tax, tay);
ctx.lineTo(pb.x + Math.cos(angle) * 0.5, pb.y + Math.sin(angle) * 0.5);
ctx.lineTo(tbx, tby);
ctx.stroke();
if (obj.start_shape == 2 || obj.start_shape == 4)
ctx.fill();
}
if (obj.end_shape > 0)
{
let pa = obj.points[obj.points.length - 2];
let pb = obj.points[obj.points.length - 1];
let dx = pb.x - pa.x;
let dy = pb.y - pa.y;
let angle = Math.atan2(dy, dx);
const baseSize = 3 + shapeSize;
let tax = pb.x - Math.cos(angle - Math.PI/6) * baseSize;
let tay = pb.y - Math.sin(angle - Math.PI/6) * baseSize;
let tbx = pb.x - Math.cos(angle + Math.PI/6) * baseSize;
let tby = pb.y - Math.sin(angle + Math.PI/6) * baseSize;
ctx.beginPath();
ctx.moveTo(tax, tay);
ctx.lineTo(pb.x + Math.cos(angle) * 0.5, pb.y + Math.sin(angle) * 0.5);
ctx.lineTo(tbx, tby);
ctx.stroke();
if (obj.end_shape == 2 || obj.end_shape == 4)
ctx.fill();
}
ctx.lineWidth = 1;
}
for (let obj of doc.objects.filter((o) => o instanceof AltiumPolygon))
{
if (!this.#shouldShow(obj)) continue;
ctx.strokeStyle = this.#altiumColourToHex(obj.line_colour);
ctx.fillStyle = this.#altiumColourToHex(obj.fill_colour);
ctx.lineWidth = obj.width;
ctx.beginPath();
ctx.moveTo(obj.points[0].x, obj.points[0].y);
for (let i = 1; i < obj.points.length; i++)
{
ctx.lineTo(obj.points[i].x, obj.points[i].y);
}
ctx.closePath();
ctx.stroke();
ctx.fill();
ctx.lineWidth = 1;
}
for (let obj of doc.objects.filter((o) => o instanceof AltiumJunction))
{
if (!this.#shouldShow(obj)) continue;
ctx.fillStyle = this.#altiumColourToHex(obj.colour);
ctx.beginPath();
ctx.ellipse(obj.x, obj.y, 2, 2, 0, 0, 2*Math.PI);
ctx.fill();
}
for (let obj of doc.objects.filter((o) => o instanceof AltiumPowerPort))
{
if (!this.#shouldShow(obj)) continue;
ctx.strokeStyle = this.#altiumColourToHex(obj.colour);
ctx.fillStyle = this.#altiumColourToHex(obj.colour);
ctx.lineWidth = 1;
if (!obj.is_off_sheet_connector)
{
switch (obj.style)
{
case 2:
ctx.beginPath();
ctx.moveTo(obj.x, obj.y);
ctx.lineTo(obj.x, obj.y + 10);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(obj.x - 5, obj.y + 10);
ctx.lineTo(obj.x + 5, obj.y + 10);
ctx.stroke();
break;
case 4:
ctx.beginPath();
ctx.moveTo(obj.x - 10, obj.y);
ctx.lineTo(obj.x + 10, obj.y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(obj.x - 7.5, obj.y - 2);
ctx.lineTo(obj.x + 7.5, obj.y - 2);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(obj.x - 5, obj.y - 4);
ctx.lineTo(obj.x + 5, obj.y - 4);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(obj.x - 2.5, obj.y - 6);
ctx.lineTo(obj.x + 2.5, obj.y - 6);
ctx.stroke();
break;
case 6:
ctx.beginPath();
ctx.moveTo(obj.x, obj.y);
ctx.lineTo(obj.x, obj.y - 5);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(obj.x - 5, obj.y - 5);
ctx.lineTo(obj.x + 5, obj.y - 5);
ctx.stroke();
for (let g = -1; g < 2; g++)
{
ctx.beginPath();
ctx.moveTo(obj.x + (g * 5), obj.y - 5);
ctx.lineTo(obj.x + (g * 5) - 3, obj.y - 10);
ctx.stroke();
}
break;
default:
ctx.fillRect(obj.x - 10, obj.y, 20, (obj.orientation == 1) ? 10 : -10);
break;
}
}
else
{
ctx.save();
ctx.translate(obj.x, obj.y);
ctx.rotate((obj.orientation - 1) * Math.PI/2);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(-5, 5);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(5, 5);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, 5);
ctx.lineTo(-5, 10);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, 5);
ctx.lineTo(5, 10);
ctx.stroke();
ctx.restore();
}
//ctx.fillText(obj.style.toString(), obj.x, obj.y);
}
// store the transform for recovery later
let savedTransform = ctx.getTransform();
ctx.resetTransform();
for (let obj of doc.objects.filter((o) => o instanceof AltiumLabel))
{
if (!this.#shouldShow(obj)) continue;
if (obj.hidden)
continue;
ctx.textAlign = ["left", "center", "right"][obj.justification];
ctx.textBaseline = ["bottom", "bottom", "top", "top"][obj.orientation];
ctx.fillStyle = this.#altiumColourToHex(obj.colour);
ctx.save();
ctx.translate(obj.x, canvas.height - obj.y);
ctx.rotate(obj.orientation * -Math.PI/2);
ctx.fillText(obj.text, 0, 0);
ctx.restore();
}
ctx.textAlign = "left";
ctx.textBaseline = "bottom";
for (let obj of doc.objects.filter((o) => o instanceof AltiumDesignator))
{
if (!this.#shouldShow(obj)) continue;
if (obj.hidden)
continue;
ctx.textAlign = ["left", "left", "right", "right"][obj.orientation];
ctx.textBaseline = ["bottom", "bottom", "top", "top"][obj.orientation];
ctx.fillStyle = this.#altiumColourToHex(obj.colour);
ctx.fillText(obj.full_designator, obj.x, canvas.height - obj.y);
}
ctx.textAlign = "left";
ctx.textBaseline = "bottom";
for (let obj of doc.objects.filter((o) => o instanceof AltiumParameter))
{
if (!this.#shouldShow(obj)) continue;
if (obj.hidden || obj.is_implementation_parameter)
continue;
ctx.textAlign = ["left", "left", "right", "right"][obj.orientation];
ctx.textBaseline = ["bottom", "bottom", "top", "top"][obj.orientation];
ctx.fillStyle = this.#altiumColourToHex(obj.colour);
if (obj.orientation == 1)
{
ctx.save();
ctx.translate(obj.x, canvas.height - obj.y);
ctx.rotate(-Math.PI/2);
ctx.fillText(obj.text, 0, 0);
ctx.restore();
}
else if (obj.orientation == 3)
{
ctx.save();
ctx.translate(obj.x, canvas.height - obj.y);
ctx.rotate(Math.PI/2);
ctx.fillText(obj.text, 0, 0);
ctx.restore();
}
else
{
ctx.fillText(obj.text, obj.x, canvas.height - obj.y);
}
}
ctx.textAlign = "left";
ctx.textBaseline = "bottom";
for (let obj of doc.objects.filter((o) => o instanceof AltiumNetLabel))
{
if (!this.#shouldShow(obj)) continue;
if (obj.hidden)
continue;
ctx.textAlign = ["left", "center", "right"][obj.justification];
ctx.textBaseline = ["bottom", "bottom", "top", "top"][obj.orientation];
ctx.fillStyle = this.#altiumColourToHex(obj.colour);
if (obj.orientation == 1)
{
ctx.save();
ctx.translate(obj.x, canvas.height - obj.y);
ctx.rotate(-Math.PI/2);
ctx.fillText(obj.text, 0, 0);
ctx.restore();
}
else if (obj.orientation == 3)
{
ctx.save();
ctx.translate(obj.x, canvas.height - obj.y);
ctx.rotate(Math.PI/2);
ctx.fillText(obj.text, 0, 0);
ctx.restore();
}
else
{
ctx.fillText(obj.text, obj.x, canvas.height - obj.y);
}
}
ctx.textAlign = "left";
ctx.textBaseline = "bottom";
for (let obj of doc.objects.filter((o) => o instanceof AltiumPin))
{
if (!this.#shouldShow(obj)) continue;
if (!obj.show_name)
continue;
ctx.textAlign = ["right", "right", "left", "right"][obj.orientation];
ctx.textBaseline = "middle";
let objName = obj.name;
let inverted = false;
if (obj.name.includes("\\"))
{
objName = obj.name.replaceAll("\\", "");
inverted = true;
}
if (obj.name_orientation != 0)
{
ctx.textBaseline = ["middle", "top", "middle", "bottom"][obj.orientation];
if (obj.name_orientation <= 3)
ctx.textAlign = ["left", "center", "right"][obj.name_orientation-1];
else
ctx.textAlign = "center";
}
let margin_x = [-1, 0, 1, 0][obj.orientation] * 2;
let margin_y = [0, -1, 0, 1][obj.orientation] * 2;
ctx.fillStyle = this.#altiumColourToHex(obj.colour);
ctx.strokeStyle = ctx.fillStyle;
ctx.lineWidth = 1;
if (obj.orientation == 1 && obj.name_orientation == 0)
{
ctx.save();
ctx.translate(obj.x + margin_x, canvas.height - (obj.y + margin_y));
ctx.rotate(-Math.PI/2);
ctx.fillText(objName, 0, 0);
if (inverted)
{
// todo: test this
let textSize = ctx.measureText(objName);
ctx.beginPath();
ctx.moveTo(0, textSize.actualBoundingBoxAscent + 2);
ctx.lineTo(textSize.width, textSize.actualBoundingBoxAscent + 2);
ctx.stroke();
}
ctx.restore();
}
else if (obj.orientation == 3 && obj.name_orientation == 0)
{
ctx.save();
ctx.translate(obj.x + margin_x, canvas.height - (obj.y + margin_y));
ctx.rotate(Math.PI/2);
ctx.fillText(objName, 0, 0);
if (inverted)
{
// todo: test this
let textSize = ctx.measureText(objName);
ctx.beginPath();
ctx.moveTo(0, textSize.actualBoundingBoxAscent + 2);
ctx.lineTo(textSize.width, textSize.actualBoundingBoxAscent + 2);
ctx.stroke();
}
ctx.restore();
}
else
{
ctx.fillText(objName, obj.x + margin_x, canvas.height - (obj.y + margin_y));
if (inverted)
{
let textSize = ctx.measureText(objName);
let offset = 0;
switch (ctx.textAlign)
{
case "center":
offset = -(textSize.width/2);
break;
case "right":
offset = -textSize.width;
break;
case "left":
offset = 0;
break;
default:
offset = 0;
break;
}
ctx.beginPath();
ctx.moveTo(obj.x + margin_x + offset, canvas.height - (obj.y + margin_y + textSize.actualBoundingBoxAscent + 2));
ctx.lineTo(obj.x + margin_x + offset + textSize.width, canvas.height - (obj.y + margin_y + textSize.actualBoundingBoxAscent + 2));
ctx.stroke();
}
}
ctx.setLineDash([]);
}
ctx.textAlign = "left";
ctx.textBaseline = "bottom";
for (let obj of doc.objects.filter((o) => o instanceof AltiumPowerPort))
{
if (!this.#shouldShow(obj)) continue;
if (!obj.show_text)
continue;
ctx.fillStyle = this.#altiumColourToHex(obj.colour);
ctx.textBaseline = ["middle", "top", "middle", "bottom"][obj.orientation];
ctx.textAlign = ["left", "center", "right", "center"][obj.orientation];
let offset_x = [12, 0, -12, 0][obj.orientation];
let offset_y = [0, 20, 0, -20][obj.orientation];
ctx.fillText(obj.text, obj.x + offset_x, canvas.height - (obj.y + offset_y));
}
ctx.textAlign = "left";
ctx.textBaseline = "middle";
let savedFont = ctx.font;
for (let obj of doc.objects.filter((o) => o instanceof AltiumTextFrame))
{
if (!this.#shouldShow(obj)) continue;
if (obj.font_id > 0 && doc.sheet.fonts[obj.font_id] != null)
{
const frameFont = doc.sheet.fonts[obj.font_id];
const fontStr = (frameFont.size - 1).toString() + "px " + frameFont.name;
if (fontStr.includes(":") || fontStr.includes("/") || !document.fonts.check(fontStr))
{
ctx.font = savedFont;
}
else
{
ctx.font = fontStr;
}
}
ctx.fillStyle = this.#altiumColourToHex(obj.text_colour);
ctx.textAlign = ["center", "left", "right"][obj.alignment];
let offset_x = [(obj.right-obj.left)/2, obj.text_margin, (obj.right-obj.left) - obj.text_margin][obj.alignment];
if (!obj.word_wrap)
{
ctx.fillText(obj.text.replaceAll("~1", "\n"), obj.left + offset_x, canvas.height - (obj.top + (obj.bottom-obj.top)/2));
}
else
{
// todo: refactor this so that an initial pass figures out all the line splits, then a second pass writes the text, so that vertical alignment can be supported.
const text = obj.text.replaceAll("~1", "\n");
const lines = text.split("\n");
let ypos = 0;
if (lines.length > 1)
{
// this is a total hack, but if there are multiple lines in the text then we can make a rough guess at how far up we need to shift the text to center it vertically
// this doesn't correct for line wraps (see todo above for refactoring approach) but it's at least something I guess!
const roughMeasure = ctx.measureText(text);
ypos = ((roughMeasure.fontBoundingBoxDescent + roughMeasure.fontBoundingBoxAscent) * -lines.length) / 2;
}
const maxWidth = (obj.right - obj.left) + (obj.text_margin * 2);
for (let line of lines)
{
const lineMeasure = ctx.measureText(line);
if (lineMeasure.width <= maxWidth)
{
ctx.fillText(line, obj.left + offset_x, (canvas.height - (obj.top + (obj.bottom-obj.top)/2)) + ypos);
ypos += lineMeasure.fontBoundingBoxDescent + lineMeasure.fontBoundingBoxAscent;
}
else
{
let words = line.split(" ");
while (words.length > 0)
{
if (words.length == 1)
{
// we only have one word, either because that's just how many we had or because the final word is super long
const lastWord = words[0];
const lastWordMeasure = ctx.measureText(lastWord);
ctx.fillText(lastWord, obj.left + offset_x, (canvas.height - (obj.top + (obj.bottom-obj.top)/2)) + ypos);
ypos += lastWordMeasure.fontBoundingBoxDescent + lineMeasure.fontBoundingBoxAscent;
words = [];
break;
}
for (let wc = words.length; wc > 0; wc--)
{
const partialLine = words.slice(0, wc - 1).join(" ");
const partialMeasure = ctx.measureText(partialLine);
if (partialMeasure.width <= maxWidth || wc == 1)
{
ctx.fillText(partialLine, obj.left + offset_x, (canvas.height - (obj.top + (obj.bottom-obj.top)/2)) + ypos);
ypos += partialMeasure.fontBoundingBoxDescent + lineMeasure.fontBoundingBoxAscent;
words = words.slice(wc - 1);
break;
}
}
}
}
}
}
}
ctx.font = savedFont;
ctx.textAlign = "left";
ctx.textBaseline = "bottom";
ctx.setTransform(savedTransform);
savedFont = ctx.font;
ctx.textAlign = "left";
ctx.font = "bold 33px sans-serif";
ctx.fillStyle = "#000000";
ctx.globalAlpha = 0.2;
ctx.save();
ctx.scale(1,-1);
ctx.fillText("Preview generated by altium.js", 10, -(canvas.height - 50));
ctx.font = "bold 15px sans-serif";
ctx.fillText("for reference purposes only. schematic accuracy not guaranteed.", 12, -(canvas.height - 75));
ctx.restore();
ctx.globalAlpha = 1;
ctx.font = savedFont;
}
}

94
base64_binary.js Normal file
View File

@ -0,0 +1,94 @@
/*
Copyright (c) 2011, Daniel Guerrero
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL DANIEL GUERRERO BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* Uses the new array typed in javascript to binary base64 encode/decode
* at the moment just decodes a binary base64 encoded
* into either an ArrayBuffer (decodeArrayBuffer)
* or into an Uint8Array (decode)
*
* References:
* https://developer.mozilla.org/en/JavaScript_typed_arrays/ArrayBuffer
* https://developer.mozilla.org/en/JavaScript_typed_arrays/Uint8Array
*/
var Base64Binary = {
_keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
/* will return a Uint8Array type */
decodeArrayBuffer: function(input) {
var bytes = (input.length/4) * 3;
var ab = new ArrayBuffer(bytes);
this.decode(input, ab);
return ab;
},
removePaddingChars: function(input){
var lkey = this._keyStr.indexOf(input.charAt(input.length - 1));
if(lkey == 64){
return input.substring(0,input.length - 1);
}
return input;
},
decode: function (input, arrayBuffer) {
//get last chars to see if are valid
input = this.removePaddingChars(input);
input = this.removePaddingChars(input);
var bytes = parseInt((input.length / 4) * 3, 10);
var uarray;
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
var j = 0;
if (arrayBuffer)
uarray = new Uint8Array(arrayBuffer);
else
uarray = new Uint8Array(bytes);
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
for (i=0; i<bytes; i+=3) {
//get the 3 octects in 4 ascii chars
enc1 = this._keyStr.indexOf(input.charAt(j++));
enc2 = this._keyStr.indexOf(input.charAt(j++));
enc3 = this._keyStr.indexOf(input.charAt(j++));
enc4 = this._keyStr.indexOf(input.charAt(j++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
uarray[i] = chr1;
if (enc3 != 64) uarray[i+1] = chr2;
if (enc4 != 64) uarray[i+2] = chr3;
}
return uarray;
}
}

93
helper_extensions.js Normal file
View File

@ -0,0 +1,93 @@
/*
altium.js schematic document parser - helper extensions
Copyright (c) 2022 Graham Sutherland
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
Uint8Array.prototype.compare_to = function(to, src_offset=0)
{
if (to == null)
{
return false;
}
let target_len = 0;
if (to instanceof ArrayBuffer)
{
target_len = to.byteLength;
}
else if (to instanceof Uint8Array)
{
target_len = to.length;
}
else
{
return false;
}
if (src_offset + target_len > this.length)
{
return false;
}
for (let i = 0; i < target_len; i++)
{
if (this[src_offset+i] != to[i])
return false;
}
return true;
}
ArrayBuffer.prototype.compare_to = function(to, src_offset=0)
{
if (to == null)
{
return false;
}
let target_len = 0;
if (to instanceof ArrayBuffer)
{
target_len = to.byteLength;
}
else if (to instanceof Uint8Array)
{
target_len = to.length;
}
else
{
return false;
}
if (src_offset + target_len > this.byteLength)
{
return false;
}
for (let i = 0; i < target_len; i++)
{
if (this[src_offset+i] != to[i])
return false;
}
return true;
}

189
ole.js Normal file
View File

@ -0,0 +1,189 @@
/*
altium.js OLE parser
Copyright (c) 2022 Graham Sutherland
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
class OLE
{
static get ExpectedMagicNumber() { return new Uint8Array([ 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1 ]) }
static get ExpectedMajorVersion() { return 0x0003; }
static get ExpectedMinorVersion() { return 0x003E; }
static get ExpectedByteOrder() { return 0xFFFE; }
static get ExpectedSectorShift() { return 0x0009; }
static get ExpectedMiniSectorShift() { return 0x0006; }
static get ExpectedNumberOfDirectorySectors() { return 0; }
constructor(fileData)
{
this.stream = new U8Stream(fileData);
if (fileData.length < 76)
{
throw new Error("File is too small to be a valid OLE compound document.");
}
this.header = {};
this.header.signature = this.stream.read(8);
if (!this.header.signature.compare_to(OLE.ExpectedMagicNumber))
{
throw new Error("File does not start with the correct OLE header signature.");
}
this.header.clsid = this.stream.read(16);
if (!this.header.clsid.every((b) => b === 0))
{
console.warn("OLE header CLSID was not set to all zeroes.");
}
this.header.minor_version = this.stream.read_u16_le();
this.header.major_version = this.stream.read_u16_le();
if (this.header.major_version != OLE.ExpectedMajorVersion || this.header.minor_version != OLE.ExpectedMinorVersion)
{
throw new Error("OLE header does not have expected OLE version number.");
}
this.header.byte_order = this.stream.read_u16_le();
if (this.header.byte_order != OLE.ExpectedByteOrder)
{
throw new Error("OLE header does not have expected byte order value.");
}
this.header.sector_shift = this.stream.read_u16_le();
if (this.header.sector_shift != OLE.ExpectedSectorShift)
{
throw new Error("OLE header does not have expected sector shift value.");
}
this.header.sector_size = 1 << this.header.sector_shift;
this.header.mini_sector_shift = this.stream.read_u16_le();
if (this.header.mini_sector_shift != OLE.ExpectedMiniSectorShift)
{
throw new Error("OLE header does not have expected mini sector shift value.");
}
this.header.mini_sector_size = 1 << this.header.mini_sector_shift;
if (!this.stream.read(6).every((b) => b === 0))
{
console.warn("OLE header reserved field was not set to all zeroes.");
}
// directory sectors are unsupported for V3
this.header.directory_sector_count = this.stream.read_u32_le();
if (this.header.directory_sector_count != OLE.ExpectedNumberOfDirectorySectors)
{
throw new Error("OLE header does not have expected number of directory sectors.");
}
this.header.fat_sector_count = this.stream.read_u32_le();
this.header.first_directory_sector_location = this.stream.read_u32_le();
this.header.transaction_signature_number = this.stream.read_u32_le();
this.header.mini_stream_cutoff_size = this.stream.read_u32_le();
this.header.fist_mini_fat_sector_location = this.stream.read_u32_le();
this.header.number_of_mini_fat_sectors = this.stream.read_u32_le();
this.header.first_difat_sector_location = this.stream.read_u32_le();
this.header.number_of_difat_sectors = this.stream.read_u32_le();
this.header.difat = new Uint32Array(this.stream.read(436, true));
this.fat_sectors = [];
for (let i = 0; i < 109; i++)
{
this.fat_sectors[i] = new FATSector(this, this.header.difat[i]);
}
this.directory_entries = [];
this.directory_entries[0] = new DirectoryEntry(this, (this.header.first_directory_sector_location + 1) * this.header.sector_size);
this.directory_entries[1] = new DirectoryEntry(this, (this.header.first_directory_sector_location + 1) * this.header.sector_size + 128);
this.directory_entries[2] = new DirectoryEntry(this, (this.header.first_directory_sector_location + 1) * this.header.sector_size + 256);
this.directory_entries[3] = new DirectoryEntry(this, (this.header.first_directory_sector_location + 1) * this.header.sector_size + 384);
}
}
class FATSector
{
constructor(ole, sector_location)
{
this.ole = ole;
this.sector_location = sector_location;
const typeMap = [
{ value: 0xFFFFFFFC, name: "DIFSECT" },
{ value: 0xFFFFFFFD, name: "FATSECT" },
{ value: 0xFFFFFFFE, name: "ENDOFCHAIN" },
{ value: 0xFFFFFFFF, name: "FREESECT" },
];
if (this.sector_location >= 0xFFFFFFC)
{
this.entries = [];
this.type = typeMap.find(tm => tm.value == this.sector_location).name;
}
else
{
this.type = "FAT";
ole.stream.seek_to((this.sector_location + 1) * ole.header.sector_size);
this.entries = new Uint32Array(ole.stream.read(ole.header.sector_size - 4, true));
this.next_difat_sector = ole.stream.read_u32_le();
}
}
}
class DirectoryEntry
{
constructor(ole, position)
{
this.ole = ole;
this.position = position;
ole.stream.seek_to(position);
let name_bytes = ole.stream.read(64);
let name_len = ole.stream.read_u16_le();
if (name_len > 64)
{
throw new Error("Name length field exceeded maximum size of 64.");
}
this.name_field_length = name_len;
this.name = new TextDecoder("utf-16").decode(name_bytes.slice(0, name_len));
if (this.name.endsWith("\u0000"))
this.name = this.name.slice(0, this.name.indexOf("\u0000"));
this.object_type = ole.stream.read_u8();
this.colour_flag = ole.stream.read_u8();
this.left_sibling = ole.stream.read_u32_le();
this.right_sibling = ole.stream.read_u32_le();
this.child_id = ole.stream.read_u32_le();
this.clsid = ole.stream.read(16);
this.state_bits = ole.stream.read_u32_le();
this.creation_time = ole.stream.read_u64_le();
this.modified_time = ole.stream.read_u64_le();
this.starting_sector_location = ole.stream.read_u32_le();
this.stream_size = ole.stream.read_u64_le();
}
read_all()
{
this.ole.stream.seek_to((this.starting_sector_location + 1) * this.ole.header.sector_size);
return new U8Stream(this.ole.stream.read(Number(this.stream_size)));
}
}

4
test_schdoc.js Normal file

File diff suppressed because one or more lines are too long

178
u8stream.js Normal file
View File

@ -0,0 +1,178 @@
/*
altium.js schematic document parser - u8stream helper
Copyright (c) 2022 Graham Sutherland
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
class U8Stream extends Uint8Array
{
u8stream_position = 0;
reset()
{
this.u8stream_position = 0;
}
seek_to(pos)
{
if (pos == null || !(typeof pos === 'number') || !Number.isInteger(pos))
{
throw new Error("Position must be an integer.");
}
if (pos < 0)
{
throw new Error("Attempted to seek to a negative position.");
}
if (pos >= this.length)
{
throw new Error("Attempted to seek past the end of the array.");
}
this.u8stream_position = pos;
}
seek_relative(distance)
{
if (distance == null || !(typeof distance === 'number') || !Number.isInteger(distance))
{
throw new Error("Distance must be an integer.");
}
let pos = this.u8stream_position ?? 0;
pos += distance;
if (pos < 0)
{
throw new Error("Attempted to seek to a negative position.");
}
if (pos >= this.length)
{
throw new Error("Attempted to seek past the end of the array.");
}
this.u8stream_position = pos;
}
read(length, useBuffer=false)
{
if (length == null || !(typeof length === 'number') || !Number.isInteger(length))
{
throw new Error("Length must be an integer.");
}
if (length < 0)
{
throw new Error("Attempted to read a negative number of bytes.");
}
if (length == 0)
return useBuffer ? new ArrayBuffer([]) : new Uint8Array([]);
let pos = this.u8stream_position ?? 0;
if (pos + length > this.length)
{
throw new Error("Attempted to read past the end of the array.");
}
let target = useBuffer ? this.buffer : this;
let result = target.slice(pos, pos + length);
this.u8stream_position = pos + length;
return result;
}
read_u8()
{
return this.read(1)[0];
}
read_u16(littleEndian)
{
if (this.u8stream_position + 2 > this.length)
{
throw new Error("Attempted to read past the end of the array.");
}
if (this.bufferView == null)
{
this.bufferView = new DataView(this.buffer);
}
let value = this.bufferView.getUint16(this.u8stream_position, littleEndian);
this.u8stream_position += 2;
return value;
}
read_u16_be()
{
return this.read_u16(false);
}
read_u16_le()
{
return this.read_u16(true);
}
read_u32(littleEndian)
{
if (this.u8stream_position + 4 > this.length)
{
throw new Error("Attempted to read past the end of the array.");
}
if (this.bufferView == null)
{
this.bufferView = new DataView(this.buffer);
}
let value = this.bufferView.getUint32(this.u8stream_position, littleEndian);
this.u8stream_position += 4;
return value;
}
read_u32_be()
{
return this.read_u32(false);
}
read_u32_le()
{
return this.read_u32(true);
}
read_u64(littleEndian)
{
if (this.u8stream_position + 8 > this.length)
{
throw new Error("Attempted to read past the end of the array.");
}
if (this.bufferView == null)
{
this.bufferView = new DataView(this.buffer);
}
let value = this.bufferView.getBigUint64(this.u8stream_position, littleEndian);
this.u8stream_position += 8;
return value;
}
read_u64_be()
{
return this.read_u64(false);
}
read_u64_le()
{
return this.read_u64(true);
}
}