{"id":24499629,"url":"https://github.com/codewithkyle/pslib","last_synced_at":"2026-02-03T06:02:44.700Z","repository":{"id":259279600,"uuid":"876909503","full_name":"codewithkyle/pslib","owner":"codewithkyle","description":"A print-ready PostScript file fabricator","archived":false,"fork":false,"pushed_at":"2024-11-03T14:30:05.000Z","size":52,"stargazers_count":2,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-10-23T14:58:25.092Z","etag":null,"topics":["eps","postscript","printing","ps"],"latest_commit_sha":null,"homepage":"https://crates.io/crates/pslib","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/codewithkyle.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-10-22T18:54:00.000Z","updated_at":"2025-08-02T01:30:19.000Z","dependencies_parsed_at":"2024-10-24T03:36:44.521Z","dependency_job_id":"9734f7b8-885d-4ee9-9527-c70244896357","html_url":"https://github.com/codewithkyle/pslib","commit_stats":null,"previous_names":["codewithkyle/pslib"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/codewithkyle/pslib","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codewithkyle%2Fpslib","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codewithkyle%2Fpslib/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codewithkyle%2Fpslib/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codewithkyle%2Fpslib/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/codewithkyle","download_url":"https://codeload.github.com/codewithkyle/pslib/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codewithkyle%2Fpslib/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29035250,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-03T02:28:16.591Z","status":"ssl_error","status_checked_at":"2026-02-03T02:27:48.904Z","response_time":96,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["eps","postscript","printing","ps"],"created_at":"2025-01-21T22:15:10.383Z","updated_at":"2026-02-03T06:02:44.674Z","avatar_url":"https://github.com/codewithkyle.png","language":"Rust","readme":"\u003e [!CAUTION]\n\u003e This library is under development. Documentation, functionality, and implementation details are not final. **Do NOT use in production.**\n\n## Overview\n\n```rust\nuse pslib::{ Document, Page, Rect, Line }\nuse std::{path::Path, fs::OpenOptions, io::BufWriter};\n\npub fn main() {\n\n    // Prepare the output (boilerplate)\n    let path = Path::new(\"output.ps\");\n    let file = OpenOptions::new()\n                    .write(true)\n                    .create(true)\n                    .open(path)?;\n    let mut writer = BufWriter::new(\u0026file);\n\n    // Create a new PostScript document (adds PostScript boilerplate)\n    let doc = Document::new(writer);\n\n    // Create a page: (w, h)\n    let page = Page::new(400, 400);\n\n    // Create a rectangle: (x, y, w, h)\n    let rect = Rect::new(0, 0, 100, 100)\n                    .fill_rgb(1.0, 0.0, 0.0);\n\n    // Add rectangle to page (generates PS \u0026 writes to internal page buffer)\n    page.add(\u0026rect);\n\n    // Create a line: (x1, y1, x2, y2)\n    let line = Line::new(0, 100, 200, 100)\n                    .stroke_rgb(1, 0.0, 0.0, 0.1);\n\n    // Anything that impls the Serialize trait can be added to the page\n    page.add(\u0026line);\n\n    // Add page to document (flushes internal page buffer to BufWriter)\n    // Anything that impls Fabricate trait can be added to the document\n    doc.add(\u0026page);\n\n    // Appends EOF\n    doc.close();\n}\n```\n\n## Serialize Trait\n\nAnything that implements the `Serialize` trait can be added to a `Page` struct instance. The `Serialize` trait is used to convert a data structure to a multi-line PostScript string.\n\n```rust\npub trait Serialize {\n    fn to_postscript_string(\u0026self) -\u003e String;\n}\n```\n\n### Example\n\n```rust\nstruct Rect {\n    x: i32,\n    y: i32,\n    width: i32,\n    height: i32,\n    strokeWidth: u8,\n    strokeColor: [3; f32],\n    fillColor: [3; f32],\n}\n\nimpl Serialize for Rect {\n    fn to_postscript_string(\u0026self) -\u003e String {\n        let mut result = String::new();\n        \n        result.push_str(\"newpath\\n\");\n        \n        write!(\u0026mut result, \"{} {} moveto\\n\", self.x, self.y).unwrap();\n        \n        write!(\u0026mut result, \"0 {} rlineto\\n\", self.height).unwrap();\n        write!(\u0026mut result, \"{} 0 rlineto\\n\", self.width).unwrap();\n        write!(\u0026mut result, \"0 -{} rlineto\\n\", self.height).unwrap();\n        write!(\u0026mut result, \"-{} 0 rlineto\\n\", self.width).unwrap();\n        \n        // Add the closepath and stroke commands\n        result.push_str(\"closepath\\n\");\n\n        if self.strokeWidth \u003e 0 {\n            write!(\u0026mut result, \"{} setlinewidth\\n\", self.strokeWidth).unwrap();\n            write!(\u0026mut result, \"{} {} {} setrgbcolor\\n\", self.strokeColor[0], self.strokeColor[1], self.strokeColor[2]).unwrap();\n            result.push_str(\"stroke\\n\");\n        }\n        \n        result\n    }\n}\n```\n\n## Fabricate Trait\n\nAnything that implements the `Fabricate` trait can be added to a `Document`. The `Fabricate` trait is used when you need to merge buffers (eg: writing a `Page` to a PostScript file).\n\n```rust\npub trait Fabricate {\n    fn fabricate\u003cW: Write\u003e(\u0026self, doc_type: \u0026DocumentType, writer: \u0026mut BufWriter\u003cW\u003e) -\u003e Result\u003c(), Error\u003e;\n}\n```\n\n### Example\n\nAppending a `Page` onto a `Document`.\n\n```rust\nstruct Page {\n    width: i32,\n    height: i32,\n    buffer: Vec\u003cu8\u003e,\n}\n\nimpl Fabricate for Page {\n    fn fabricate\u003cW: Write\u003e(\u0026self, doc_type: \u0026DocumentType, writer: \u0026mut BufWriter\u003cW\u003e) -\u003e Result\u003c(), Error\u003e {\n        match doc_type {\n            DocumentType::PS =\u003e {\n                write!(\n                    writer,\n                    r#\"\n                        %%PageBoundingBox: 0 0 {} {}\n                        \u003c\u003c /PageSize [{} {}] \u003e\u003e setpagedevice\n                    \"#,\n                    self.width, self.height, self.width, self.height\n                )?;\n                writer.write_all(\u0026self.buffer)?;\n                writer.write_all(\"showpage\\n\".as_bytes())?;\n            }\n            _ =\u003e {\n                writer.write_all(\u0026self.buffer)?;\n            }\n        }\n        Ok(())\n    }\n}\n```\n\n## Document\n\nDocuments support writing to any type of buffer that implements the `Write` trait. Common usage includes:\n\n- `File`\n- `Vec\u003cu8\u003e`\n- `stdout`\n- `TcpStream`\n\nWhen using `Document::new()` the document will be initialized with the default PostScript procedures defined by this library. If you need to define custom procedures see the document builder pattern section below.\n\n```rust\nuse pslib::{ Document, Page };\n\nfn main() {\n    let path = Path::new(\"output.ps\");\n    let file = OpenOptions::new()\n                    .write(true)\n                    .create(true)\n                    .open(path)?;\n    let mut writer = BufWriter::new(\u0026file);\n    let doc = Document::new(writer);\n    let page = Page::new(400, 400);\n    doc.add(\u0026page);\n}\n```\n\n### Document Types\n\nThis library supports creating both PostScript and Encapsulated PostScript. Documents will default to PostScript when using `Document::new()`\n\n```rust\nenum DocumentType {\n    PS, // PostScript\n    EPS, // Encapsulated PostScript\n}\n```\n\n### Builder\n\nWhen creating a `Document` you can use the builder pattern.\n\n```rust\nuse pslib::DocumentBuilder;\n\nfn main() {\n    let doc = Document::builder().build();\n}\n```\n\n#### Setting the documents type\n\nThe `document_type()` method allows you to set a specific document type.\n\n```rust\nlet doc = Document::builder().document_type(DocumentType::EPS).build();\n```\n\n#### Setting the documents buffer writer\n\nWhen using the builder pattern the default the `BufWriter` will write to a `Vec\u003cu8\u003e` buffer. The `writer()` method allows you to set a specific buffer writer.\n\n```rust\nuse pslib::{ DocumentBuilder, ProcedureRegistry };\n\nfn main() {\n    let path = Path::new(\"output.ps\");\n    let file = OpenOptions::new()\n                    .write(true)\n                    .create(true)\n                    .open(path)?;\n    let mut doc = DocumentBuilder::builder()\n            .document_type(DocumentType::EPS)\n            .writer(BufWriter::new(\u0026file))\n            .load_procedures(ProcedureRegistry::with_builtins())\n            .bounding_box(500, 300)\n            .build();\n}\n```\n\n#### Loading procedures\n\nThe `load_procedures()` method allows you to initialize the document with a set of prebuilt PostScript procedures using the `ProcedureRegistry`.\n\n```rust\nlet doc = DocumentBuilder::builder().load_procedures(ProcedureRegistry::with_builtins()).build();\n```\n\n## Procedures\n\nPostScript allows us to define procedures that it pushes onto the operand stack (see [PLRM page 32-33](https://www.adobe.com/jp/print/postscript/pdfs/PLRM.pdf). These procedures can be repeatably executed to perform a predefine set of operations. \n\nUtilizing procedures increases interpreter performance while also reducing the overall final file size.\n\n### Builtin Procedures\n\n```rust\nlet registry = ProcedureRegistry::with_builtins();\nlet proc_rect = registry.get_procedure(\"rect\").unwrap();\nprintln!(\"{}\", proc_rect);\n```\n\n```postscript\n% ex: -100 0 0 -100 100 0 0 100 300 300 rect\n/rect {\n    newpath\n    moveto\n    rlineto\n    rlineto\n    rlineto\n    rlineto\n    closepath\n} def\n```\n\n### Adding Custom Procedures\n\n```rust\nuse pslib::{ ProcedureRegistry, Procedure };\n\nfn main() {\n    let registry = ProcedureRegistry::new();\n    registry.add_procedure(Procedure {\n        name: \"custom_shape\".to_string(),\n        body: r#\"\n            /custom_shape {\n                % Custom PostScript code\n            } def\n        \"#.to_string(),\n    });\n}\n```\n## Line\n\n```rust\nuse pslib::{ Line, TransformLineOrigin };\n\nfn main() {\n    let line = Line::new(100.0, 100.0, 100.0)\n        .rotate(45.0)\n        .set_orign(TransformLineOrigin::Left)\n        .stroke_cmyk(2.0, 1.0, 0.0, 0.0, 0.25);\n}\n```\n\n| Method | Parameters |\n| - | - |\n| `stroke_rgb` | `(width: f32, r: f32, g: f32, b: f32)` |\n| `stroke_cmyk` | `(width: f32, c: f32, m: f32, y: f32, k: f32)` |\n| `scale` | `(x: f32, y: f32)` |\n| `set_orign` | `(origin: TransformOrigin)` |\n| `rotate` | `(angle: f32)` |\n\n## Rect\n\n```rust\nuse pslib::Rect;\n\nfn main() {\n    let rect = Rect::new(155.0, 155.0, 100.0, 100.0)\n        .fill_rgb(1.0, 0.0, 0.0)\n        .rotate(45.0)\n        .scale(1.5, 1.0)\n        .stroke_rgb(2.0, 0.0, 0.0, 0.0);\n}\n```\n\n| Method | Parameters |\n| - | - |\n| `fill_rgb` | `(r: f32, g: f32, b: f32)` |\n| `fill_cmyk` | `(c: f32, m: f32, y: f32, k: f32)` |\n| `stroke_rgb` | `(width: f32, r: f32, g: f32, b: f32)` |\n| `stroke_cmyk` | `(width: f32, c: f32, m: f32, y: f32, k: f32)` |\n| `scale` | `(x: f32, y: f32)` |\n| `set_orign` | `(origin: TransformOrigin)` |\n| `rotate` | `(angle: f32)` |\n\n## Text\n\n\u003e [!WARNING]\n\u003e Text structure and implemention pending.\n\n```rust\nstruct Text {\n    x: f32,\n    y: f32,\n    width: f32,\n    height: f32,\n    stroke_width: f32,\n    stroke_color_rgb: [f32; 3],\n    stroke_color_cmyk: [f32; 4],\n    fill_color_rgb: [f32; 3],\n    fill_color_cmyk: [f32; 4],\n    text: String,\n    rotate: f32,\n    scale: [f32; 2],\n    horizontal_align: TextHorizontalAlignment,\n    vertical_align: TextVerticalAlignment,\n    fit: TextFit,\n    wrap: TextWrap,\n}\n\nenum TextFit {\n    Contain, // default\n    Stretch,\n    StretchHorizontal,\n    StretchVertical,\n    Crop,\n}\n\nenum TextHorizontalAlignment {\n    Left, // default\n    Center,\n    Right,\n}\n\nenum TextVerticalAlignment {\n    Top, // default\n    Center,\n    Bottom,\n}\n\nenum TextWrap {\n    Wrap,\n    Nowrap, // default\n}\n```\n\n### Example\n\n```rust\nlet text = Text::new(\"Hello, World!\", 0.0, 0.0, 100.0, 50.0)\n                    .horizontal_align(TextHorizontalAlignment::Center)\n                    .vertical_align(TextVerticalAlignment::Center)\n                    .fit(TextFit::StretchHorizontal)\n                    .wrap(TextWrap::Wrap);\n```\n\n## Custom Fonts\n\nDetails pending.\n\n## Image\n\n\u003e [!WARNING]\n\u003e Image structure and implemention pending.\n\nI'm thinking I'd like images to be automatically conveted to PostScript procedures. That way if/when we draw the same image to the document several times we don't have to binary encode the image every time.\n\nMy first instinct is to create another registry like the `ProcedureRegistry` but this would force developers to register all the images they're going to use up front.\n\nExperiment needed: test to see if the code that invokes a procedure can come before the definition of the procedure.\n\n```rust\nstruct Image {\n    x: f32,\n    y: f32,\n    width: f32,\n    height: f32,\n    rotate: f32,\n    scale: [f32; 2],\n    procedure_id: String,\n    fit: ImageFit,\n}\n\nenum ImageFit {\n    Contain, // default\n    Stretch,\n    Crop,\n    Copyonly,\n}\n\nimpl Image {\n    pub fn new(procedure_id: String, x: f32, y: f32, width: f32, height: f32) -\u003e Self {\n        Image {\n            x: x.max(0.0),\n            y: y.max(0.0),\n            width: width.max(0.0),\n            height: height.max(0.0),\n            rotate: 0.0,\n            scale: [0.0, 0.0],\n            procedure_id: procedure_id,\n            fit: ImageFit::Contain,\n        }\n    }\n}\n```\n\n### Image Registry\n\n```rust\nstruct RawImage {\n    file_name: String,\n    file_path: Path,\n    procedure_name: String,\n}\nstruct ImageRegistry {\n    images: HashMap\u003cString, RawImage\u003e,\n    count: u32,\n}\n\nimpl ImageRegistry {\n    pub fn new() -\u003e Self {\n        ImageRegistry {\n            images: HashMap::new(),\n            count: 0,\n        }\n    }\n\n    pub fn add(mut self, path: Path) -\u003e Self {\n        let file_name = path.file_name().to_string_lossy();\n        self.count += 1;\n        let proc_name = format!(\"imager{}\", self.count);\n        let image = RawImage {\n            file_name: file_name,\n            file_path: path,\n            procedure_name: proc_name,\n        };\n        self.images.add(file_name, image);\n        self\n    }\n\n    pub fn get_procedure_id(self, file_name: String) -\u003e Option\u003cString\u003e {\n        let raw = self.images.get(file_name);\n        if raw.is_none() {\n            return None;\n        }\n        Some(raw.procedure_name)\n    }\n}\n```\n\n### Example\n\n```rust\nlet image_path = Path::new(\"/some/directory/filename.txt\");\nlet registry = ProcedureRegistry::new();\nregistry.add(image_path);\n\nlet buffer: Vec\u003cu8\u003e = Vec::new();\nlet mut writer = BufWriter::new(\u0026buffer);\nlet document = Document::new(writer);\n\ndocument.load_images(registry); // generates and writes image procedures to buffer\n\nlet page = Page::new(100.0, 100.0);\n\nlet image = Image::new(registry.get_procedure_id(\"filename.txt\", 0.0, 0.0, 100.0, 100.0));\npage.add(\u0026image); // writes \"100 100 0 0 imager1\" to buffer\n```\n\n## Inline Images\n\n\u003e [!WARNING]\n\u003e Inline image structure and implemention pending.\n\nUnlike the standard `Image` that invokes a stored image procedure defiend by the `ImageRegistry` inline images will write the binary encoded image directy into the `Page` every time. This will most likely be useful when a developer _knows_ they will only write the image once. It may also be useful (even recommended?) when creating EPS files.\n\nThe final documentation should strongly encourage developers to use the `ImageRegistry`.\n\n```rust\nstruct InlineImage {\n    x: f32,\n    y: f32,\n    width: f32,\n    height: f32,\n    rotate: f32,\n    scale: [f32; 2],\n    file_path: Path,\n    fit: ImageFit,\n}\n\nimpl InlineImage {\n    pub fn new(file_path: Path, x: f32, y: f32, width: f32, height: f32) -\u003e Self {\n        InlineImage {\n            x: x.max(0.0),\n            y: y.max(0.0),\n            width: width.max(0.0),\n            height: height.max(0.0),\n            rotate: 0.0,\n            scale: [0.0, 0.0],\n            file_path: file_path,\n            fit: ImageFit::Contain,\n        }\n    }\n}\n```","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodewithkyle%2Fpslib","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcodewithkyle%2Fpslib","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodewithkyle%2Fpslib/lists"}