From e252ddb952d5cd764e7ee45cd78f669e7e6cf859 Mon Sep 17 00:00:00 2001 From: Steve Dogiakos Date: Thu, 12 Mar 2026 10:29:36 -0600 Subject: [PATCH] Add full project structure: backend, frontend, Docker, and CI workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Organize backend into src/ (routes/, services/, db/) per package.json entrypoint - Add migrations/import-mdb.js for one-time .mdb → SQLite migration - Add public/ frontend: check ledger table, slide-in new/edit panel, PDF generation - Add docker/Dockerfile and docker-compose.yml for self-hosted deployment - Add .github/workflows: Docker Hub build+push on main/tags, TODO→Issues scanner - Add GnuMICR font files (GPL-2.0) for MICR E-13B line rendering --- .env.example | 1 + .github/workflows/docker-build.yml | 53 ++ .github/workflows/todo-to-issues.yml | 28 + .gitignore | 11 + LICENSE | 4 + README.md | 87 +- TODO.md | 65 ++ docker-compose.yml | 22 + docker/Dockerfile | 18 + fonts/AUTHORS | 3 + fonts/CHANGELOG | 26 + fonts/COPYING | 339 ++++++++ fonts/GnuMICR.afm | 36 + fonts/GnuMICR.pfa | 215 +++++ fonts/GnuMICR.pfb | Bin 0 -> 5803 bytes fonts/GnuMICR.pfm | Bin 0 -> 674 bytes fonts/GnuMICR.raw | 1189 ++++++++++++++++++++++++++ fonts/INSTALL | 27 + fonts/LICENSE | 3 + fonts/NEWS | 15 + fonts/README | 93 ++ fonts/comparison.png | Bin 0 -> 6068 bytes fonts/symbols.png | Bin 0 -> 5030 bytes fonts/test.ps | 14 + migrations/import-mdb.js | 374 ++++++++ package.json | 22 + public/css/style.css | 313 +++++++ public/index.html | 122 +++ public/js/app.js | 391 +++++++++ src/app.js | 41 + src/db/database.js | 26 + src/db/schema.sql | 82 ++ src/routes/checks.js | 139 +++ src/routes/pdf.js | 62 ++ src/services/pdfService.js | 292 +++++++ 35 files changed, 4112 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 .github/workflows/docker-build.yml create mode 100644 .github/workflows/todo-to-issues.yml create mode 100644 TODO.md create mode 100644 docker-compose.yml create mode 100644 docker/Dockerfile create mode 100644 fonts/AUTHORS create mode 100644 fonts/CHANGELOG create mode 100644 fonts/COPYING create mode 100644 fonts/GnuMICR.afm create mode 100644 fonts/GnuMICR.pfa create mode 100644 fonts/GnuMICR.pfb create mode 100644 fonts/GnuMICR.pfm create mode 100644 fonts/GnuMICR.raw create mode 100644 fonts/INSTALL create mode 100644 fonts/LICENSE create mode 100644 fonts/NEWS create mode 100644 fonts/README create mode 100644 fonts/comparison.png create mode 100644 fonts/symbols.png create mode 100644 fonts/test.ps create mode 100644 migrations/import-mdb.js create mode 100644 package.json create mode 100644 public/css/style.css create mode 100644 public/index.html create mode 100644 public/js/app.js create mode 100644 src/app.js create mode 100644 src/db/database.js create mode 100644 src/db/schema.sql create mode 100644 src/routes/checks.js create mode 100644 src/routes/pdf.js create mode 100644 src/services/pdfService.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7adb4b8 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +MICR_FONT_PATH=/app/fonts/micrenc.ttf \ No newline at end of file diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..388f738 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,53 @@ +name: Build and push Docker image + +on: + push: + branches: [main] + tags: ['v*'] + workflow_dispatch: + +env: + IMAGE_NAME: ${{ secrets.DOCKERHUB_USERNAME }}/ezcheck + +jobs: + build-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract image metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix=sha- + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/todo-to-issues.yml b/.github/workflows/todo-to-issues.yml new file mode 100644 index 0000000..3c54bbf --- /dev/null +++ b/.github/workflows/todo-to-issues.yml @@ -0,0 +1,28 @@ +name: TODO to Issues + +on: + push: + branches: [main] + +jobs: + todo: + runs-on: ubuntu-latest + # Skip on merge commits from the action itself to avoid loops + if: "!contains(github.event.head_commit.message, '[bot]')" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Scan TODOs and create issues + uses: alstr/todo-to-issue-action@v5 + with: + TOKEN: ${{ secrets.GITHUB_TOKEN }} + CLOSE_ISSUES: true + AUTO_ASSIGN: true + IDENTIFIERS: | + [ + {"name": "TODO", "labels": ["enhancement"]}, + {"name": "FIXME", "labels": ["bug"]}, + {"name": "HACK", "labels": ["technical-debt"]} + ] diff --git a/.gitignore b/.gitignore index 9a5aced..e101264 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,14 @@ dist # Vite logs files vite.config.js.timestamp-* vite.config.ts.timestamp-* + +data/ +*.db +*.db-shm +*.db-wal +fonts/*.ttf +fonts/*.otf + +#AI +CLAUDE.md +/.claude diff --git a/LICENSE b/LICENSE index 9a26c01..5fc32d9 100644 --- a/LICENSE +++ b/LICENSE @@ -19,3 +19,7 @@ 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. +--- + +fonts/GnuMICR.otf is licensed separately under the GNU General Public +License v2.0. See fonts/LICENSE for details. diff --git a/README.md b/README.md index 0b70c4e..a6f3e9e 100644 --- a/README.md +++ b/README.md @@ -1 +1,86 @@ -# check-printing \ No newline at end of file +# ezcheck + +Self-hosted web app for printing checks on blank check stock. Replaces ezCheckPrinting (Halfpricesoft). + +## Stack + +- **Runtime:** Node.js 20 +- **Framework:** Express +- **Database:** SQLite via `better-sqlite3` +- **PDF generation:** PDFKit with embedded MICR E-13B font +- **Frontend:** Vanilla JS, no framework +- **Container:** Docker Compose + +## Project Structure + +``` +ezcheck/ +├── src/ +│ ├── routes/ +│ │ ├── checks.js # CRUD for check records +│ │ ├── accounts.js # Account config (Phase 2) +│ │ └── pdf.js # PDF generation endpoint +│ ├── services/ +│ │ └── pdfService.js # PDFKit rendering logic +│ ├── db/ +│ │ ├── schema.sql # SQLite schema +│ │ └── database.js # DB connection + helpers +│ └── app.js # Express app +├── migrations/ +│ └── import-mdb.js # One-time .mdb import script +├── public/ +│ ├── css/style.css +│ ├── js/app.js +│ └── index.html +├── fonts/ # MICR E-13B TTF goes here +├── docker/ +│ └── Dockerfile +├── docker-compose.yml +├── package.json +└── .env.example +``` + +## Getting Started + +### Development (local) + +```bash +npm install +cp .env.example .env +node migrations/import-mdb.js --file /path/to/YourAccount.mdb +npm run dev +``` + +### Production (Docker) + +```bash +docker compose up -d +``` + +## Migration + +The import script reads a single `.mdb` file and populates the SQLite database. +It requires `mdbtools` to be installed on the host or available in the container. + +```bash +node migrations/import-mdb.js --file "Montana Dinosaur Center.mdb" +``` + +## Printing + +- Select 1–3 checks from the ledger +- Click "Print PDF" +- App generates a 3-up 8.5"×11" PDF (three 3.667" check slots) +- PDF opens in browser, user sends to printer +- Checks are marked as printed in the ledger + +## Check Layout Coordinate Space + +Coordinates are in inches. Origin is top-left of each check slot. +Check slot dimensions: 8.5" wide × 3.667" tall (three per letter page). +MICR line is hardcoded at Y = 3.4" (0.267" from bottom of slot). + +## MICR Font + +Place `micrenc.ttf` or `GnuMICR.ttf` in the `fonts/` directory. +Update `MICR_FONT_PATH` in `.env` if using a different filename. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..7880cef --- /dev/null +++ b/TODO.md @@ -0,0 +1,65 @@ +# TODO + +## MVP + +- [ ] Build `public/index.html` -- app shell, header with company name and current check number +- [ ] Build `public/css/style.css` -- functional, dense layout; ledger table is primary view +- [ ] Build `public/js/app.js` -- all frontend logic via `fetch()` +- [ ] Check ledger table -- columns: check #, date, payee, amount, memo, printed status +- [ ] Ledger: filter by printed / unprinted +- [ ] Ledger: sort by check number and date +- [ ] New check form -- fields: payee, amount, date, memo, note1, note2; address fields collapsed by default +- [ ] New check form -- slide-in panel or modal, not a separate page +- [ ] Edit mode for unprinted checks (inline or same panel as new check form) +- [ ] Checkbox selection of 1--3 checks for print; enforce the 3-check maximum in the UI +- [ ] "Generate PDF" button -- POST to `/api/pdf`, open resulting PDF in a new browser tab +- [ ] Reprint flow -- allow re-generating PDF for already-printed checks without re-marking (`?mark_printed=false`) +- [ ] Delete confirmation for unprinted checks +- [ ] Basic error display for API failures (failed PDF generation, validation errors) +- [ ] Amount input validation -- numeric, two decimal places, greater than zero +- [ ] Date input defaults to today +- [ ] Check number display -- show next check number on new check form (read from account) +- [ ] Run migration against `Montana Dinosaur Center.mdb` and verify all check records import correctly +- [ ] Verify PDF output: spot-check field positions against a printed check from the original software +- [ ] Verify MICR line renders using GnuMICR.otf and lands at correct Y position +- [ ] Docker Compose smoke test -- confirm app starts, DB initializes, and PDF endpoint responds + +--- + +## Post-MVP / Future Features + +These exist in the original ezCheckPrinting software but are intentionally out of scope for MVP. + +### Multi-account support +- [ ] Account switcher -- the original software has ~14 accounts across TMDC, Lions, District 37, etc. +- [ ] `account_id` foreign key on `checks` and `layout_fields` tables +- [ ] Account management UI -- create, edit, delete accounts +- [ ] Per-account logo and signature image upload +- [ ] Per-account bank configuration (routing, account number, transit code, company info) + +### Check layout editor +- [ ] Visual layout editor -- drag or nudge field positions (X/Y in inches) +- [ ] Per-field font, size, and visibility toggles +- [ ] Printer offset calibration UI (offset left/right/up/down) for aligning to check stock +- [ ] Preview panel that reflects layout changes before saving + +### Payee management +- [ ] Payee address book -- store and recall payee name + address lines +- [ ] Autocomplete payee field from address book on new check form + +### Check stub +- [ ] 1-up with stub layout (alternative to 3-up) -- stub fields are already in `layout_fields` +- [ ] Stub field rendering in PDF service (fields prefixed `Stub` in `layout_fields`) + +### Reporting and ledger features +- [ ] Date range filter on ledger +- [ ] Search/filter by payee name +- [ ] Total amount display for filtered ledger view +- [ ] CSV export of check ledger + +### Import / migration +- [ ] Multi-account `.mdb` import -- run migration script per account, associate with account record +- [ ] Import logo from `.mdb` `Settings` table for accounts that have one (already implemented in script, needs UI trigger) + +### Authentication +- [ ] Basic auth or simple password gate for any deployment that leaves the local network diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c780f22 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +services: + ezcheck: + build: + context: . + dockerfile: docker/Dockerfile + container_name: ezcheck + restart: unless-stopped + ports: + - "3000:3000" + volumes: + # Persistent data: SQLite DB lives here + - ezcheck-data:/app/data + - ./fonts:/app/fonts:ro + environment: + - NODE_ENV=production + - PORT=3000 + - DB_PATH=/app/data/ezcheck.db + # Full path to MICR font inside container + - MICR_FONT_PATH=${MICR_FONT_PATH} + +volumes: + ezcheck-data: diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..665268c --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,18 @@ +FROM node:20-alpine + +# mdbtools for migration script (only needed on first run, stays in image for convenience) +RUN apk add --no-cache mdbtools + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci --omit=dev + +COPY . . + +# Data volume: SQLite database and any runtime uploads +VOLUME ["/app/data"] + +EXPOSE 3000 + +CMD ["node", "src/app.js"] diff --git a/fonts/AUTHORS b/fonts/AUTHORS new file mode 100644 index 0000000..78c4564 --- /dev/null +++ b/fonts/AUTHORS @@ -0,0 +1,3 @@ +GnuMICR was created by: + + Eric Sandeen diff --git a/fonts/CHANGELOG b/fonts/CHANGELOG new file mode 100644 index 0000000..5f52084 --- /dev/null +++ b/fonts/CHANGELOG @@ -0,0 +1,26 @@ + +GnuMICR Changelog + +December 10, 2003 +0.30 Re-generated the TTF font using pfaedit (so I can recreate) + Generated otf font file using pfaedit + Added StdVW to StemSnapV array (Thanks t1lint!) + Fixed typo IsFixedPitch -> isFixedPitch + Rebuilt pfa and pfb with t1utils v. 1.29 + Removed blather about distribution w/ a commercial + application, that's not consistent w/ the GPL. + Updated contact information. + +August 12, 2000 +0.22 Included a converted TTF font (also untested) + +August 2, 2000 +0.21 Added Unique ID number for the font + Added some copyright information + +July 2, 2000 +0.2 Recompiled fonts with 64 char lines (oops) + +July 1, 2000 +0.1 First public release + diff --git a/fonts/COPYING b/fonts/COPYING new file mode 100644 index 0000000..a43ea21 --- /dev/null +++ b/fonts/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 675 Mass Ave, Cambridge, MA 02139, USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + Appendix: How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) 19yy + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) 19yy name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/fonts/GnuMICR.afm b/fonts/GnuMICR.afm new file mode 100644 index 0000000..92cbd2f --- /dev/null +++ b/fonts/GnuMICR.afm @@ -0,0 +1,36 @@ +StartFontMetrics 2.0 +Comment Generated by pfaedit +Comment Creation Date: Thu Dec 11 12:40:09 2003 +FontName GnuMICR +FullName GnuMICR +FamilyName GnuMICR +Weight Normal +Notice (Copyright (c) 2000-2003, Eric Sandeen . Released under the terms of the Gnu Public License, www.gnu.org) +ItalicAngle 0 +IsFixedPitch false +UnderlinePosition -100 +UnderlineThickness 50 +Version 000.300 +EncodingScheme FontSpecific +FontBBox 10 0 720 702 +StartCharMetrics 18 +C 0 ; WX 500 ; N .notdef ; B 0 0 0 0 ; +C 32 ; WX 751 ; N space ; B 0 0 0 0 ; +C 48 ; WX 751 ; N zero ; B 103 0 649 702 ; +C 49 ; WX 751 ; N one ; B 337 0 649 702 ; +C 50 ; WX 751 ; N two ; B 337 0 649 702 ; +C 51 ; WX 751 ; N three ; B 259 0 649 702 ; +C 52 ; WX 751 ; N four ; B 181 0 649 702 ; +C 53 ; WX 751 ; N five ; B 259 0 649 702 ; +C 54 ; WX 751 ; N six ; B 181 0 649 702 ; +C 55 ; WX 751 ; N seven ; B 259 0 649 702 ; +C 56 ; WX 751 ; N eight ; B 103 0 649 702 ; +C 57 ; WX 751 ; N nine ; B 181 0 649 702 ; +C 65 ; WX 751 ; N A ; B 103 0 649 702 ; +C 66 ; WX 751 ; N B ; B 103 0 649 702 ; +C 67 ; WX 751 ; N C ; B 103 117 649 663 ; +C 68 ; WX 751 ; N D ; B 103 195 649 507 ; +C 169 ; WX 751 ; N copyright ; B 10 10 720 120 ; +C -1 ; WX 500 ; N CR ; B 0 0 0 0 ; +EndCharMetrics +EndFontMetrics diff --git a/fonts/GnuMICR.pfa b/fonts/GnuMICR.pfa new file mode 100644 index 0000000..0db27e3 --- /dev/null +++ b/fonts/GnuMICR.pfa @@ -0,0 +1,215 @@ +%!FontType1-1.1: GnuMICR 000.300 +%%CreationDate: Wed Aug 02 19:41:00 2000 +%%VMusage: 120000 150000 +%(The above line is most likely not correct) +% +%--------------- +% +% GnuMICR - a free implementation of the MICR font +% +% Copyright (C) 2000-2003 Eric Sandeen (sandeen-gnumicr@sandeen.net) +% +% This program is free software; you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation; either version 2 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program; if not, write to the Free Software +% Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +% +%--------------- +% +% This font contains only the digits 0-9, and 4 symbols +% To get the symbols, use the characters A B C D +% +% To convert back and forth between an editable raw font file +% and an encoded, encrypted, useable Type 1 font file, you will need +% the t1utils package from http://www.lcdf.org/~eddietwo/type/ +% +%--------------- +% +% TODO: +% Get this thing inspected by a bank...! +% Implement stem hint replacement ("3", "8", Symbols) +% Angles/arcs on "7" may not be quite right +% +%--------------- +% +11 dict begin +/FontInfo 14 dict dup begin +/version (000.300) readonly def +/Copyright (Copyright 2000-2003, Eric Sandeen) readonly def +/Notice (Copyright (c) 2000-2003, Eric Sandeen . Released under the terms of the Gnu Public License, www.gnu.org) readonly def +/FullName (GnuMICR) readonly def +/FamilyName (GnuMICR) readonly def +/Weight (Normal) readonly def +/ItalicAngle 0 def +/isFixedPitch false def +/UnderlinePosition -100 def +/UnderlineThickness 50 def +end readonly def + +/FontName /GnuMICR def +/PaintType 0 def +/FontType 1 def +/FontMatrix [ 0.00100 0 0 0.00100 0 0 ] readonly def +/Encoding 256 array +0 1 255 {1 index exch /.notdef put } for +dup 32 /space put +dup 48 /zero put +dup 49 /one put +dup 50 /two put +dup 51 /three put +dup 52 /four put +dup 53 /five put +dup 54 /six put +dup 55 /seven put +dup 56 /eight put +dup 57 /nine put +dup 65 /A put +dup 66 /B put +dup 67 /C put +dup 68 /D put +dup 169 /copyright put +readonly def + +/FontBBox { 103 0 649 702 } readonly def +currentdict end +currentfile eexec +d9d66f633b846a989b9974b0179fc6cc445bcf7c3c3333173232e3fdbff43949 +1db866c39088c203dc22fdc758584860ec7bb67fda28cc6208249060e18fab32 +204779b5c03c0493bbbbc95cf02692cc4deaa8d2ea90b5c2e64374e92bcb8501 +429b8fae4a76c0c6b76d6ff7cf9a7d5edfbca0e959541c59bd05b7de43d25d53 +fc3dda6ef0c2743978a6d03e19cced4a11f2ea4bcc3110be8b8d9e2772361969 +c19258efafdc276cb1ade9208a941a36d18f9fb1c33def76aa3140a8a4c99adb +b3214e61cb091bb87421cef35ff5745ef8dab2327f4505291949c5c81a2c61a1 +c68bc0c2724684a65c13e201889ed4829f0502939b52213248c263db1fe10129 +8a2904757b2fb30240088b194cd883e258e163d6ecf1d233c50b7245be021177 +eac51a7a1a807977163b3a28a3c9f79d5116de6525552753c1d3aa7bb4ec7b18 +837ef3428d84afc12dd6cb1f27e859ffb74c151b97b4a6ff35710cdce68fcbaa +ab650eeeffc0c2bc940fabcd1da75ad2bd6cb6e0567455fb1de69f17d1474b1f +be7eec173206d8b4a85a24d9cfb113b2ebf1e8790e6e02c643418d0a8d19a5bf +48528e6a4d92065c9b68886a9093256bedf9a90ecbedcbaa21f5194ef7e7cdb2 +68b296d436bcb83abeb6d0e64b498d3fa1d4f67418d72f1ffcbefcd0bc198738 +46537a5f6147e88a422a858ea0d8035cb03be90cc441ebe8f38fad0ba587a8b8 +6106ff8c87f2add0df9bb2384795042dfce4ba9b2c49269249640a8792a966b7 +09e7f16df067017e7253e273d2b2c495d12848b56aeb76cad0a1d217fcf3ec3f +e474793c37ea3acbf375302c907e888e48c77767306c2d64f1b66d625e5f7270 +40fbfb2438ff6eba6c72759808e9556910b61627058848d1736ae22f55ff2d0f +caf500e55ede2fa5add20b2622909a1b906b68cf458ad50a33d453328e6b6d46 +0942916d050df3fda98862355b6d0f9e931ca94db56d0aef32dcb68fc2fcb1ca +3b05ec92191ef9d190dc14b70ece10a09e4b5e3745e041462f97907120e0c24d +efa34a171a43ab03a31edcca00299cf085f74356901e6e858643e060e0531f33 +227904d8d5e5e7f0ab1f2b5c97a5e45647321138d4856a1fec830e7075bdcebd +b0d1d481f6c79500619b1b3a725237aaf4879364007d48c64c19a374d2e21792 +bc0cbef879e0a348bf86a62d4d7d0526cc64ab1928a5642cd8bdbc55bf85dad8 +1a2ba2f7e4537706878f74d6b2be523f8a9de00c5d01184405487f1c873f102e +a0a9bef2f2fa9718d8cade54f5cc17aadaa78280472a1f749c20583d9c0a1106 +ad73decc7efcb4de4315683e204d7a8c0512e41e7e501b4884e4ee90d08652f3 +4c15e337e74dd4823e4aa117251c3cd02e27827f07297bf2b08a8087ccce4f3f +cd86d361e3dd8d11ba1c4b58d5838923342d8b23ae88b25c4410e4fa85eba1ac +1c676b35bc3a344525248fe25e4619e4476cdc566729217d325f88ef1b752851 +508f90bab8aca5d9382414a114a0d903b16ce7821442e01312d58b5e2e26c0df +cf0d1454678e3b9a9b157ddc49d3a7f7235666ebef29cba7360a113f540bd8ae +fac6dfc4b9d16c8e754376d8eb73f86b3469b94717251c3c2eb411f1df2acac9 +96ce622d50e289ae896b9e01227284244683504fed3bc7d32c79a1053e6883b4 +003fe9244cab401ba5566469541e7f6d10606cd188deebb7e8858900aea86ea5 +0d412a03bd075cd2d3ac38a202db7634197e4a6940ed83d32733ed4228f4a208 +316ce4daa9ebe5f14ff089ac978d0a39303aa0eaca67ac5132d592391c6d8593 +cbc678fad548ffab092d702ac1f31c3c34e7dc9c66655403fdee707c7c31b1bb +17909887c564967f49604f9eb776cb9720fead110e878e0f003261a54dc6fa01 +eec1a40ff45fefa7d85001e3b322c7447293cf20cc5d8461be725b4d75efde5e +98a9ae0008490739cd664a7defd93b444dd65ff1c7e8f8f8a3ba8b1cea0e2fbb +315a4eccbf09993c402534d2721ea64afdebdad1ece3ac1939f96cd777b17de8 +b9d527d32ce26f4e1f861ed5916da9523f058fc3cdaa367756879d5353dae748 +4bbbda7de04edd2ace4922af206631719471324e3c6b8282f01d53861389c2d6 +b54306763edefb2e049ace70526afba2d782e354ed9a2945f4a0470ed4c7b418 +3566e0bb1d94d945cb8d94e438026ff260ba5ce66d4d51fd4887214fbe26e928 +ff262e98518b92b40caa5bcfe73ea8cbf86c7ec1c89866c537242307190fed28 +23f66362aff372e7da620fdf0daa086856278b06b417964c9bb13560be9a6644 +360b70f0def155ed64bdb26f1733d6fe91a04aaf2e45534c5865768c9cb901ef +e56144ecb22046dd87d12e7a8826abfc8728249617b5bfe2850b7063136a5518 +84d244913e06f72d094b3453542ec6d6375bd3d99e5f158d5f6889229bc9c609 +fe1d0b2a511be50f7b91af54e8200f1606ca468e30b51c7d37eac9d8a640589b +14daa550358e5a6f462b0b6fa7ffa3cf79e686a9cb0bfe66d728755ca37d4fa2 +a4bc2f74b03fb31ab682bd95565b1011c6cc1437b995c1bc3c273c65904f4c9f +de274a8bd8f5bf97a1addce6194b9240a32d372451fe699b0afab349139e2168 +3928d1b65ab575347f08cd88f52a6b9cb0c5b0448fc83ac3b25c1e867888ea0f +bf9800cb5e6509fe5eee739cdc1359fd09bed912ad1997d1f2a89d124bb18e7c +a0f1078cb35f41ae0347cd900c30e871a84cce6e2ddeb210e33c5b9e6db78fd8 +fa8e61a4294c90acb1e3a4d69168bc67cde8e96272d2bbd7c890c7ba211176ad +786e99843c10b7618955a244f5f7a8795735989327124dfc25ec3a005fd84b05 +9e4ee615545555d72f6e6fc371712aa16bc6d08d1df5953d962103f3b76f0501 +e18699411aaf89f876a019516ae3ef8bdfbe31bfb1594ec0ee8ee3b7171493c8 +347e2a9b250816a45800514b7e63aa0c1094f62678732da76460a55f02528abf +0f9bd557abab16eb2927b5c68be71f466c58af23c67a7a15c82b277cc8163d74 +9517aa9ef177d3bea03aafd08d5585629cc17d97e77510a59b29316d1b67ff85 +0773244e688edc30cbf1528ade8840c5f492247b89841b8e03d047eb80d9ce49 +94d0483348697bd1f24173de4806df1208facd8f8797e1761a548b82bdfb3467 +f0b6a840812221026531d561866f693c1b28f63ca2fe4f458678e084869a381d +b37b6cd52234c28f1a69b4b611db245ff2ec1d6f82bc04d4df67a6bb7cfef7c4 +fc751ba88d114592fca0bf7ec26bfaf4909047c3621510588266d57efb10858d +9bb05064d331e4b1f6ea87feb979f6376e848eb204a7bdfc3581a6f97734c680 +c09722bf3ed67b24ec7f40e3f0afd702dc639e1541489b864bd53c4d1fc6a5ba +0cc887eb451b6fabbb028ffbc05eab5c0c5b9c111895943c6ce54eb0d94fa1d1 +ce3bab45cac7a6270d4668b6a75211515ad0401a16512b1aa22f775c2726ed25 +22247b78e865f7835d9641f7c25992a0f596b12f85400a753fbfd36c50e89036 +d5fd47bf945009a0ed48d44e7201f03511865adabc24e3f65a994fd9ef121f02 +ad010f75bb840f82d91467e5dfabd033febbb96d3a0e2e667df8c3410fd538e6 +aac9aefbcffc22eb77b86e3e609d282666d7e8e26a678cc29d256ae3b78bb993 +0580c39eba3c5f25fe02ea5dba6f872fecdd1264b32ef27d95d7d074de99233d +799992a9656cd581407b82f1025ba5fcf3ac88dc319c2c1af7e8a4479d9d1a64 +7ecac0ad08980115d1082a35d2ee9154b6e7e1ab816a1447f23bb196fbd5e022 +afb9b81b9abcd1ccfbfb95321ecc3cc3e351ff6ee4afc8da56e5ded1a4554dbf +0c2a22601441f90090cd1a5ca9f3496ea04d22b79e949e550c104deb409a0e20 +250c0455b0d4a0526d0e39c303f3a3a6efaf686ec5206bf3e595ff70d5a4346f +2a3984ce45121cf2d1928f96dca05cad7760bdf639a1cf528940240fa1748c34 +29233744bcf4f2f29c6bb830d978bec8bf6968ac6785e19df57c263ed490fc57 +392f29e6ad61702f1912b28d4da44c60ab0c1464b9c80ed4298475ea1173f48f +441cf51a7feddc28d9aa4e5369aae7f127825f69da2cb13c5a15db82863ff379 +ca0efb3ab2ee7b1c54b6254d6de4bd405af22dd455ed2369c4472ad539754ce4 +eab1e3524d7ffc75333c5e18bde6ef56d474552563ced15cdef488f661471b8b +a9bbc4c1815dccc94b40cb7e00b89b679d53188b6fbc3998b82380dbb00e37d8 +ba3e692d276eae6f4c8f3fe5c194c0d5f780496ed756b7dd6f588f786971612a +ff579c5aaf17b17f5fa640f3f5afbdb397b89a33c6a614372b74812a003a67f6 +bd6829c688fba52b924a4d4edacb02a113a57a5dd4c05badd7ac0a2b9592cec0 +e04ae2242d04ccfb445897de02e5b8b69ad754006a5a50bc21436e198faed411 +eb8d95b56162c6c239eb7adc1382f05b4ee36064d9741207f3747a4195219d61 +ca83b9717c7755dfa846f80b8b7f2356e5a4a47c4a87e83c5e9f28387c0d3a03 +7224ee2300e94bcfbb8832b54a76c043a591e611418e97f8ed9d6b94214f6ed4 +d918a9ee26fb160495b22906d40ebad0a4e270ef5d1afe3a07deca7ea084233d +8d89a346e7864646dd09205a07c9ba4d4e952152d3b99c58568691f67d9dcd8c +6a2f97e00763912f830e83029606c40c54da583bbc68eb58ea3144b1028f4763 +b5096808920788fcb80d2e868a7fb408479a4cca3a7fa1178ac907311137f3b1 +676be8a4cafc56815b05d6083d569930961ab84e3bd3541bca06df99ee407759 +d9c69f0e98bd56b8cfcce7bbae32bab38619c44fdd584af44a5448c5a0b25e59 +30ab3782dd0aef6ce50c362360a172348eb82c0df84d93f7297705513e39e760 +a68db79b1b9abcc1245e8d5cefb69494773a247127f0a5ec7d01edb35e378eab +7844363315dc364785f78919d765af8611b76fb4344ee3e9ad16014eeabc91c7 +2f6b488530ddd4f5fefa94152f1ed0fd784f14f60bbf8d175a11bbc31e2b2b40 +463df7387cc8305b172283b85e0025e208c9380898628c07726dfe7ace4f3c54 +8f14c9443bfd25d7a61897fd9e984099726df51c0092750771715787a89dcde1 +1ce64902a677fb4a1c0a54dbf851f88eb0a46cef180c118b228a3bbe3cec1c4a +8d2042f182f813a134753ebaa7b9950c121935b709f6e54015b21dac0eefadba +f284e0962d504d71489c5f7f54619c1092420ff272cd0dcb988403dc166c3b68 +8febb945ce3ebfa53208a875206cb86770cf82160fe004623ca47ebe928cfdcc +11ee9a57ffc4df10051eb7f5452f96515314be80fc1555689a32ba09b424d751 +9b2763f88e49718e5e9e3e8f022df7864edb9e1c356c141f88af9fef133fd2dc +3e51fa7973cf674a6671fc705210278564908f0f343ae56738f92277a636c669 +8a3d69014dbb1ae80f7b181712d583f744f7b975794e38250d27d6cffabee62b +5eaf1b6fa90b32fd15023d63b4fd903b569542838b6df15ceae6bad0f2c2cc99 +7592f323cab1ea7030ecc20bca409dd5a9db +0000000000000000000000000000000000000000000000000000000000000000 +0000000000000000000000000000000000000000000000000000000000000000 +0000000000000000000000000000000000000000000000000000000000000000 +0000000000000000000000000000000000000000000000000000000000000000 +0000000000000000000000000000000000000000000000000000000000000000 +0000000000000000000000000000000000000000000000000000000000000000 +0000000000000000000000000000000000000000000000000000000000000000 +0000000000000000000000000000000000000000000000000000000000000000 diff --git a/fonts/GnuMICR.pfb b/fonts/GnuMICR.pfb new file mode 100644 index 0000000000000000000000000000000000000000..a1fb5864397b496c15c77b8c09abb0f033cf6cba GIT binary patch literal 5803 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 NQSMpyZahQw{R_B4nH&HB literal 0 HcmV?d00001 diff --git a/fonts/GnuMICR.pfm b/fonts/GnuMICR.pfm new file mode 100644 index 0000000000000000000000000000000000000000..05ad51f1a8e1e22736c30083a1e6fc4c3855bb28 GIT binary patch literal 674 zcmZQzT*Sn{;GADjS(KTcQKF!ctf^pRU|^sNB#d%JY=^Vsh!teno@q(Edh=C@ffLTnlm{J&w7*ZHc07)YT kNgxJ+_e@_H@xk{%=b=MX^)Ojf(V@#D6e5GmXRx>j0A=w6&j0`b literal 0 HcmV?d00001 diff --git a/fonts/GnuMICR.raw b/fonts/GnuMICR.raw new file mode 100644 index 0000000..ef4ea10 --- /dev/null +++ b/fonts/GnuMICR.raw @@ -0,0 +1,1189 @@ +%!FontType1-1.1: GnuMICR 000.300 +%%CreationDate: Wed Aug 02 19:41:00 2000 +%%VMusage: 120000 150000 +%(The above line is most likely not correct) +% +%--------------- +% +% GnuMICR - a free implementation of the MICR font +% +% Copyright (C) 2000-2003 Eric Sandeen (sandeen-gnumicr@sandeen.net) +% +% This program is free software; you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation; either version 2 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program; if not, write to the Free Software +% Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +% +%--------------- +% +% This font contains only the digits 0-9, and 4 symbols +% To get the symbols, use the characters A B C D +% +% To convert back and forth between an editable raw font file +% and an encoded, encrypted, useable Type 1 font file, you will need +% the t1utils package from http://www.lcdf.org/~eddietwo/type/ +% +%--------------- +% +% TODO: +% Get this thing inspected by a bank...! +% Implement stem hint replacement ("3", "8", Symbols) +% Angles/arcs on "7" may not be quite right +% +%--------------- +% +11 dict begin +/FontInfo 14 dict dup begin +/version (000.300) readonly def +/Copyright (Copyright 2000-2003, Eric Sandeen) readonly def +/Notice (Copyright (c) 2000-2003, Eric Sandeen . Released under the terms of the Gnu Public License, www.gnu.org) readonly def +/FullName (GnuMICR) readonly def +/FamilyName (GnuMICR) readonly def +/Weight (Normal) readonly def +/ItalicAngle 0 def +/isFixedPitch false def +/UnderlinePosition -100 def +/UnderlineThickness 50 def +end readonly def + +/FontName /GnuMICR def +/PaintType 0 def +/FontType 1 def +/FontMatrix [ 0.00100 0 0 0.00100 0 0 ] readonly def +/Encoding 256 array +0 1 255 {1 index exch /.notdef put } for +dup 32 /space put +dup 48 /zero put +dup 49 /one put +dup 50 /two put +dup 51 /three put +dup 52 /four put +dup 53 /five put +dup 54 /six put +dup 55 /seven put +dup 56 /eight put +dup 57 /nine put +dup 65 /A put +dup 66 /B put +dup 67 /C put +dup 68 /D put +dup 169 /copyright put +readonly def + +/FontBBox { 103 0 649 702 } readonly def +currentdict end +currentfile eexec +dup /Private 18 dict dup begin +/RD { string currentfile exch readstring pop } executeonly def +/ND { noaccess def } executeonly def +/NP { noaccess put } executeonly def +/BlueValues [ 0 0 ] ND +/BlueScale 0.03963 def % default +/BlueShift 7 def % default +/BlueFuzz 1 def % default +/MinFeature { 16 16 } ND % req'd, default +/StdHW [ 78 ] ND % std horiz stem width +/StdVW [ 78 ] ND % std vert stem width +/ForceBold false def +/password 5839 def % req'd, default +/UniqueID 5116639 def % ID assigned by Adobe +/StemSnapH [ 78 156 234 ] ND % Common horiz stem widths +/StemSnapV [ 78 85 151 ] ND % Common vert stem widths +% +% The following is not used, but if we do stem hint replacement +% in the future, we'll need it. +% +/Subrs 4 array +dup 0 { + 3 0 callothersubr + pop + pop + setcurrentpoint + return + } NP +dup 1 { + 0 1 callothersubr + return + } NP +dup 2 { + 0 2 callothersubr + return + } NP +dup 3 { + return + } NP + +ND + +% Actual character definitions start here: + +2 index /CharStrings 26 dict dup begin +/.notdef { + 0 500 hsbw + endchar + } ND +/.null { + 0 0 hsbw + endchar + } ND +/CR { + 0 500 hsbw + endchar + } ND +/space { + 0 751 hsbw + endchar + } ND +/zero { + 103 751 hsbw + + 0 78 hstem + 624 78 hstem + 0 78 vstem + 468 78 vstem + + % Outline + 156 0 rmoveto + 234 hlineto + 86 70 70 86 hvcurveto + 390 vlineto + 86 -70 70 -86 vhcurveto + -234 hlineto + -86 -70 -70 -86 hvcurveto + -390 vlineto + -86 70 -70 86 vhcurveto + closepath + + % Inside path + 0 78 rmoveto + -43 -35 35 43 hvcurveto + 390 vlineto + 43 35 35 43 vhcurveto + 234 hlineto + 43 35 -35 -43 hvcurveto + -390 vlineto + -43 -35 -35 -43 vhcurveto + -234 hlineto + closepath + + endchar + } ND +/one { + 337 751 hsbw + + 0 20 hstem %ghost stem + 585 117 hstem + 78 78 vstem + + 39 0 rmoveto + 234 hlineto + 22 17 17 22 hvcurveto + 234 vlineto + 22 -17 17 -22 vhcurveto + -78 hlineto + -22 -17 17 22 hvcurveto + 312 vlineto + 22 -17 17 -22 vhcurveto + -78 hlineto + -22 -17 -17 -22 hvcurveto + -39 vlineto + -22 17 -17 22 vhcurveto + 22 17 -17 -22 hvcurveto + -195 vlineto + -22 -17 -17 -22 vhcurveto + -22 -17 -17 -22 hvcurveto + -234 vlineto + -22 17 -17 22 vhcurveto + closepath + + endchar + } ND +/two { + 337 751 hsbw + + 0 78 hstem + 312 78 hstem + 624 78 hstem + 0 78 vstem + 234 78 vstem + + 39 0 rmoveto + 234 hlineto + 22 17 17 22 hvcurveto + 22 -17 17 -22 vhcurveto + -156 hlineto + -22 -17 17 22 hvcurveto + 156 vlineto + 22 17 17 22 vhcurveto + 156 hlineto + 22 17 17 22 hvcurveto + 312 vlineto + 22 -17 17 -22 vhcurveto + -234 hlineto + -22 -17 -17 -22 hvcurveto + -22 17 -17 22 vhcurveto + 156 hlineto + 22 17 -17 -22 hvcurveto + -156 vlineto + -22 -17 -17 -22 vhcurveto + -156 hlineto + -22 -17 -17 -22 hvcurveto + -312 vlineto + -21 17 -17 21 vhcurveto + closepath + + endchar + } ND +/three { + 259 751 hsbw + + 0 78 hstem + 312 78 hstem + 624 78 hstem + 234 156 vstem + %What about skinny vstem? + + 39 0 rmoveto + 312 hlineto + 22 17 17 22 hvcurveto + 273 vlineto + 22 -17 17 -22 vhcurveto + -22 -17 17 22 hvcurveto + 273 vlineto + 22 -17 17 -22 vhcurveto + -234 hlineto + -22 -17 -17 -22 hvcurveto + -22 17 -17 22 vhcurveto + 156 hlineto + 22 17 -17 -22 hvcurveto + -156 vlineto + -22 -17 -17 -22 vhcurveto + -156 hlineto + -22 -17 -17 -22 hvcurveto + -22 17 -17 22 vhcurveto + 156 hlineto + 22 17 -17 -22 hvcurveto + -156 vlineto + -22 -17 -17 -22 vhcurveto + -156 hlineto + -22 -17 -17 -22 hvcurveto + -22 17 -17 22 vhcurveto + closepath + + endchar + } ND +/four { + 181 751 hsbw + + 0 156 vstem + 312 156 vstem + 156 78 hstem + + 351 0 rmoveto + 78 hlineto + 22 17 17 22 hvcurveto + 234 vlineto + 22 -17 17 -22 vhcurveto + -78 hlineto + -22 -17 -17 -22 hvcurveto + -22 -17 -17 -22 vhcurveto + -78 hlineto + -22 -17 17 22 hvcurveto + 390 vlineto + 22 -17 17 -22 vhcurveto + -78 hlineto + -22 -17 -17 -22 hvcurveto + -468 vlineto + -22 17 -17 22 vhcurveto + 234 hlineto + 22 17 -17 -22 hvcurveto + -78 vlineto + -22 17 -17 22 vhcurveto + closepath + + endchar + } ND +/five { + 259 751 hsbw + + 0 78 hstem + 312 78 hstem + 624 78 hstem + 0 78 vstem + 312 78 vstem + + 39 0 rmoveto + 312 hlineto + 22 17 17 22 hvcurveto + 312 vlineto + 22 -17 17 -22 vhcurveto + -234 hlineto + -22 -17 17 22 hvcurveto + 156 vlineto + 22 17 17 22 vhcurveto + 234 hlineto + 22 17 17 22 hvcurveto + 22 -17 17 -22 vhcurveto + -312 hlineto + -22 -17 -17 -22 hvcurveto + -312 vlineto + -22 17 -17 22 vhcurveto + 234 hlineto + 22 17 -17 -22 hvcurveto + -156 vlineto + -22 -17 -17 -22 vhcurveto + -234 hlineto + -22 -17 -17 -22 hvcurveto + -22 17 -17 22 vhcurveto + closepath + + endchar + } ND +/six { + 181 751 hsbw + + 0 78 hstem + 234 78 hstem + 624 78 hstem + 0 78 vstem + 234 78 vstem + 390 78 vstem + + % Outline + 39 0 rmoveto + 390 hlineto + 22 17 17 22 hvcurveto + 234 vlineto + 22 -17 17 -22 vhcurveto + -312 hlineto + -22 -17 17 22 hvcurveto + 234 vlineto + 22 17 17 22 vhcurveto + 78 hlineto + 22 17 -17 -22 hvcurveto + -39 vlineto + -22 17 -17 22 vhcurveto + 22 17 17 22 hvcurveto + 117 vlineto + 22 -17 17 -22 vhcurveto + -234 hlineto + -22 -17 -17 -22 hvcurveto + -624 vlineto + -22 17 -17 22 vhcurveto + closepath + + % Inside path + 78 78 rmoveto + -22 -17 17 22 hvcurveto + 78 vlineto + 22 17 17 22 vhcurveto + 234 hlineto + 22 17 -17 -22 hvcurveto + -78 vlineto + -22 -17 -17 -22 vhcurveto + -234 hlineto + closepath + + endchar + } ND +/seven { + 259 751 hsbw + + 624 78 hstem + 0 78 vstem + 156 78 vstem + 312 78 vstem + + 195 0 rmoveto + 22 17 17 22 hvcurveto + 261 vlineto + 0 16 9 14 15 6 rrcurveto + 108 43 rlineto + 15 6 9 14 0 16 rrcurveto + 248 vlineto + 22 -17 17 -22 vhcurveto + -312 hlineto + -22 -17 -17 -22 hvcurveto + -156 vlineto + -22 17 -17 22 vhcurveto + 22 17 17 22 hvcurveto + 78 vlineto + 22 17 17 22 vhcurveto + 156 hlineto + 22 17 -17 -22 hvcurveto + -117 vlineto + 0 -16 -9 -14 -15 -6 rrcurveto %arc + -108 -45 rlineto + -15 -6 -9 -14 0 -16 rrcurveto + -312 vlineto + -22 17 -17 22 vhcurveto + closepath + + endchar + } ND +/eight { + 103 751 hsbw + + 0 78 hstem + 312 78 hstem + 624 78 hstem + 0 156 vstem + 390 156 vstem + %vstem hints only for "fat" vstems... + + % Outline + 39 0 rmoveto + 468 hlineto + 22 17 17 22 hvcurveto + 273 vlineto + 22 -17 17 -22 vhcurveto + -22 -17 17 22 hvcurveto + 273 vlineto + 22 -17 17 -22 vhcurveto + -312 hlineto + -22 -17 -17 -22 hvcurveto + -273 vlineto + -22 -17 -17 -22 vhcurveto + -22 -17 -17 -22 hvcurveto + -273 vlineto + -22 17 -17 22 vhcurveto + closepath + + % Inside bottom path + 156 78 rmoveto + -22 -17 17 22 hvcurveto + 156 vlineto + 22 17 17 22 vhcurveto + 156 hlineto + 22 17 -17 -22 hvcurveto + -156 vlineto + -22 -17 -17 -22 vhcurveto + -156 hlineto + closepath + + % Inside top path + 0 312 rmoveto + -22 -17 17 22 hvcurveto + 156 vlineto + 22 17 17 22 vhcurveto + 156 hlineto + 22 17 -17 -22 hvcurveto + -156 vlineto + -22 -17 -17 -22 vhcurveto + -156 hlineto + closepath + + endchar + } ND +/nine { + 181 751 hsbw + + 0 20 hstem %ghost stem...? + 312 78 hstem + 624 78 hstem + 0 78 vstem + 312 156 vstem + + % Outline + 351 0 rmoveto + 78 hlineto + 22 17 17 22 hvcurveto + 624 vlineto + 22 -17 17 -22 vhcurveto + -390 hlineto + -22 -17 -17 -22 hvcurveto + -312 vlineto + -22 17 -17 22 vhcurveto + 234 hlineto + 22 17 -17 -22 hvcurveto + -234 vlineto + -22 17 -17 22 vhcurveto + closepath + + % Inside path + 0 390 rmoveto + -234 hlineto + -22 -17 17 22 hvcurveto + 156 vlineto + 22 17 17 22 vhcurveto + 234 hlineto + 22 17 -17 -22 hvcurveto + -156 vlineto + -22 -17 -17 -22 vhcurveto + closepath + + endchar + } ND +/A { + 103 751 hsbw + + 0 234 hstem + 468 234 hstem + % Stem replacement for left bar? + 0 156 vstem + 312 234 vstem + + % Left bar + 39 117 rmoveto + 78 hlineto + 22 17 17 22 hvcurveto + 390 vlineto + 22 -17 17 -22 vhcurveto + -78 hlineto + -22 -17 -17 -22 hvcurveto + -390 vlineto + -22 17 -17 22 vhcurveto + closepath + + % Bottom square + 312 -117 rmoveto + 156 hlineto + 22 17 17 22 hvcurveto + 156 vlineto + 22 -17 17 -22 vhcurveto + -156 hlineto + -22 -17 -17 -22 hvcurveto + -156 vlineto + -22 17 -17 22 vhcurveto + closepath + + % Top square + 0 468 rmoveto + 156 hlineto + 22 17 17 22 hvcurveto + 156 vlineto + 22 -17 17 -22 vhcurveto + -156 hlineto + -22 -17 -17 -22 hvcurveto + -156 vlineto + -22 17 -17 22 vhcurveto + closepath + + endchar + } ND +/B { + 103 751 hsbw + + 0 312 hstem + 390 312 hstem + % Use stem replacement for inner bar htstem? + 0 156 vstem + 234 78 vstem + 390 156 vstem + + % lower bar + 39 0 rmoveto + 78 hlineto + 22 17 17 22 hvcurveto + 234 vlineto + 22 -17 17 -22 vhcurveto + -78 hlineto + -22 -17 -17 -22 hvcurveto + -234 vlineto + -22 17 -17 22 vhcurveto + closepath + + % middle bar + 234 176 rmoveto + 22 17 17 22 hvcurveto + 273 vlineto + 22 -17 17 -22 vhcurveto + -22 -17 -17 -22 hvcurveto + -273 vlineto + -22 17 -17 22 vhcurveto + closepath + + % top bar + 156 214 rmoveto + 78 hlineto + 22 17 17 22 hvcurveto + 234 vlineto + 22 -17 17 -22 vhcurveto + -78 hlineto + -22 -17 -17 -22 hvcurveto + -234 vlineto + -22 17 -17 22 vhcurveto + closepath + + endchar + } ND +/C { + 103 751 hsbw + + 351 312 hstem + % Stem replacement for left bars? + 0 78 vstem + 156 78 vstem + 312 234 vstem + + % Left bar + 39 117 rmoveto + 22 17 17 22 hvcurveto + 390 vlineto + 22 -17 17 -22 vhcurveto + -22 -17 -17 -22 hvcurveto + -390 vlineto + -22 17 -17 22 vhcurveto + closepath + + % Middle bar + 156 0 rmoveto + 22 17 17 22 hvcurveto + 390 vlineto + 22 -17 17 -22 vhcurveto + -22 -17 -17 -22 hvcurveto + -390 vlineto + -22 17 -17 22 vhcurveto + closepath + + % Right rectangle + 156 234 rmoveto + 156 hlineto + 22 17 17 22 hvcurveto + 234 vlineto + 22 -17 17 -22 vhcurveto + -156 hlineto + -22 -17 -17 -22 hvcurveto + -234 vlineto + -22 17 -17 22 vhcurveto + closepath + + endchar + } ND +/D { + 103 751 hsbw + + 195 312 hstem + 0 156 vstem + 234 156 vstem + 468 78 vstem + + % Left bar + 39 195 rmoveto + 78 hlineto + 22 17 17 22 hvcurveto + 234 vlineto + 22 -17 17 -22 vhcurveto + -78 hlineto + -22 -17 -17 -22 hvcurveto + -234 vlineto + -22 17 -17 22 vhcurveto + closepath + + % Middle bar + 234 0 rmoveto + 78 hlineto + 22 17 17 22 hvcurveto + 234 vlineto + 22 -17 17 -22 vhcurveto + -78 hlineto + -22 -17 -17 -22 hvcurveto + -234 vlineto + -22 17 -17 22 vhcurveto + closepath + + % Right bar + 234 0 rmoveto + 22 17 17 22 hvcurveto + 234 vlineto + 22 -17 17 -22 vhcurveto + -22 -17 -17 -22 hvcurveto + -234 vlineto + -22 17 -17 22 vhcurveto + closepath + + endchar + } ND + +%% Super secret copyright notice! + +/copyright { + 0 751 hsbw + + % E + 50 10 rmoveto + 30 hlineto + 10 vlineto + -20 hlineto + 10 vlineto + 10 hlineto + 10 vlineto + -10 hlineto + 10 vlineto + 20 hlineto + 10 vlineto + -30 hlineto + -50 vlineto + closepath + + 40 0 rmoveto + + % R + 10 hlineto + 10 vlineto + 10 -10 rlineto + 10 hlineto + 10 vlineto + -10 10 rlineto + 10 hlineto + 30 vlineto + -30 hlineto + -50 vlineto + closepath + + 10 30 rmoveto + 10 vlineto + 10 hlineto + -10 vlineto + -10 hlineto + closepath + + -10 -30 rmoveto + + % I + 40 0 rmoveto + 10 hlineto + 50 vlineto + -10 hlineto + -50 vlineto + closepath + + % C + 20 0 rmoveto + 30 hlineto + 10 vlineto + -20 hlineto + 30 vlineto + 20 hlineto + 10 vlineto + -30 hlineto + -50 vlineto + closepath + + 70 0 rmoveto + + % S + 30 hlineto + 30 vlineto + -20 hlineto + 10 vlineto + 20 hlineto + 10 vlineto + -30 hlineto + -30 vlineto + 20 hlineto + -10 vlineto + -20 hlineto + -10 vlineto + closepath + + % A + 40 0 rmoveto + 10 hlineto + 20 vlineto + 10 hlineto + -20 vlineto + 10 hlineto + 50 vlineto + -30 hlineto + -50 vlineto + closepath + + 10 30 rmoveto + 10 vlineto + 10 hlineto + -10 vlineto + -10 hlineto + closepath + + -10 -30 rmoveto + + % N + 40 0 rmoveto + 10 hlineto + 30 vlineto + 10 -30 rlineto + 10 hlineto + 50 vlineto + -10 hlineto + -20 vlineto + -10 20 rlineto + -10 hlineto + -50 vlineto + closepath + + % D + 40 0 rmoveto + 20 hlineto + 10 10 rlineto + 30 vlineto + -10 10 rlineto + -20 hlineto + -50 vlineto + closepath + + 10 10 rmoveto + 30 vlineto + 10 hlineto + -30 vlineto + -10 hlineto + closepath + + -10 -10 rmoveto + + % E + 40 0 rmoveto + 30 hlineto + 10 vlineto + -20 hlineto + 10 vlineto + 10 hlineto + 10 vlineto + -10 hlineto + 10 vlineto + 20 hlineto + 10 vlineto + -30 hlineto + -50 vlineto + closepath + + 40 0 rmoveto + + % E + 30 hlineto + 10 vlineto + -20 hlineto + 10 vlineto + 10 hlineto + 10 vlineto + -10 hlineto + 10 vlineto + 20 hlineto + 10 vlineto + -30 hlineto + -50 vlineto + closepath + + 40 0 rmoveto + + % N + 10 hlineto + 30 vlineto + 10 -30 rlineto + 10 hlineto + 50 vlineto + -10 hlineto + -20 vlineto + -10 20 rlineto + -10 hlineto + -50 vlineto + closepath + + -470 60 rmoveto + + % C + 20 0 rmoveto + 30 hlineto + 10 vlineto + -20 hlineto + 30 vlineto + 20 hlineto + 10 vlineto + -30 hlineto + -50 vlineto + closepath + + 40 0 rmoveto + + % O + 30 hlineto + 50 vlineto + -30 hlineto + -50 vlineto + closepath + + 10 10 rmoveto + 10 hlineto + 30 vlineto + -10 hlineto + -30 vlineto + closepath + -10 -10 rmoveto + + 40 0 rmoveto + + % P + 10 hlineto + 20 vlineto + 20 hlineto + 30 vlineto + -30 hlineto + -50 vlineto + closepath + + 10 30 rmoveto + 10 hlineto + 10 vlineto + -10 hlineto + -10 vlineto + closepath + -10 -30 rmoveto + + 50 0 rmoveto + + % Y + 10 hlineto + 20 vlineto + 10 hlineto + 30 vlineto + -10 hlineto + -20 vlineto + -10 hlineto + 20 vlineto + -10 hlineto + -30 vlineto + 10 hlineto + -20 vlineto + closepath + + 30 0 rmoveto + + % R + 10 hlineto + 10 vlineto + 10 -10 rlineto + 10 hlineto + 10 vlineto + -10 10 rlineto + 10 hlineto + 30 vlineto + -30 hlineto + -50 vlineto + closepath + + 10 30 rmoveto + 10 vlineto + 10 hlineto + -10 vlineto + -10 hlineto + closepath + + -10 -30 rmoveto + + % I + 40 0 rmoveto + 10 hlineto + 50 vlineto + -10 hlineto + -50 vlineto + closepath + + 20 0 rmoveto + + % G + 30 hlineto + 20 vlineto + -10 hlineto + -10 vlineto + -10 hlineto + 30 vlineto + 20 hlineto + 10 vlineto + -30 hlineto + -50 vlineto + closepath + + 40 0 rmoveto + + % H + 10 hlineto + 20 vlineto + 10 hlineto + -20 vlineto + 10 hlineto + 50 vlineto + -10 hlineto + -20 vlineto + -10 hlineto + 20 vlineto + -10 hlineto + -50 vlineto + closepath + + 50 0 rmoveto + + % T + 10 hlineto + 40 vlineto + 10 hlineto + 10 vlineto + -30 hlineto + -10 vlineto + 10 hlineto + -40 vlineto + closepath + + 60 0 rmoveto + + % 2 + 30 hlineto + 10 vlineto + -20 hlineto + 10 vlineto + 20 hlineto + 30 vlineto + -30 hlineto + -10 vlineto + 20 hlineto + -10 vlineto + -20 hlineto + -30 vlineto + closepath + + 40 0 rmoveto + + % 0 + 30 hlineto + 50 vlineto + -30 hlineto + -50 vlineto + closepath + + 10 10 rmoveto + 10 hlineto + 30 vlineto + -10 hlineto + -30 vlineto + closepath + -10 -10 rmoveto + + 40 0 rmoveto + + % 0 + 30 hlineto + 50 vlineto + -30 hlineto + -50 vlineto + closepath + + 10 10 rmoveto + 10 hlineto + 30 vlineto + -10 hlineto + -30 vlineto + closepath + -10 -10 rmoveto + + + 40 0 rmoveto + + % 0 + 30 hlineto + 50 vlineto + -30 hlineto + -50 vlineto + closepath + + 10 10 rmoveto + 10 hlineto + 30 vlineto + -10 hlineto + -30 vlineto + closepath + -10 -10 rmoveto + + 40 20 rmoveto + + % - + 20 hlineto + 10 vlineto + -20 hlineto + -10 vlineto + closepath + + 30 -20 rmoveto + + % 2 + 30 hlineto + 10 vlineto + -20 hlineto + 10 vlineto + 20 hlineto + 30 vlineto + -30 hlineto + -10 vlineto + 20 hlineto + -10 vlineto + -20 hlineto + -30 vlineto + closepath + + 40 0 rmoveto + + % 0 + 30 hlineto + 50 vlineto + -30 hlineto + -50 vlineto + closepath + + 10 10 rmoveto + 10 hlineto + 30 vlineto + -10 hlineto + -30 vlineto + closepath + -10 -10 rmoveto + + 40 0 rmoveto + + % 0 + 30 hlineto + 50 vlineto + -30 hlineto + -50 vlineto + closepath + + 10 10 rmoveto + 10 hlineto + 30 vlineto + -10 hlineto + -30 vlineto + closepath + -10 -10 rmoveto + + 40 0 rmoveto + + % 3 + 30 hlineto + 50 vlineto + -30 hlineto + -10 vlineto + 20 hlineto + -10 vlineto + -20 hlineto + -10 vlineto + 20 hlineto + -10 vlineto + -20 hlineto + -10 vlineto + closepath + + endchar + } ND + +end +end +readonly put +put +dup /FontName get exch definefont pop +mark currentfile closefile diff --git a/fonts/INSTALL b/fonts/INSTALL new file mode 100644 index 0000000..edf338f --- /dev/null +++ b/fonts/INSTALL @@ -0,0 +1,27 @@ +If you're using this font under Microsoft Windows, I can't help you. + +If you're using it under a Linux-y operating system, there are documents +available which do a better job than I ever would at explaining how to +install them. + +In general, take a look at the Font-HOWTO, available from +http://www.linuxdoc.org/HOWTO/Font-HOWTO.html particularly + +"Making Fonts Available To X" +http://www.linuxdoc.org/HOWTO/Font-HOWTO-4.html + +and + +"Making Fonts Available to GhostScript" +http://www.linuxdoc.org/HOWTO/Font-HOWTO-5.html + +I have included a test PostScript(tm) file called "test.ps" that you can +fire up in GhostScript to see if you have that set up correctly. + +I have also included a file, "symbols.png" to show you which characters +you need to type to get the various MICR symbols. + +Note that to function in the MICR readers, this font must be sized +exactly right. GnuMICR is designed to be used at 12 point size. + +Good luck! diff --git a/fonts/LICENSE b/fonts/LICENSE new file mode 100644 index 0000000..d5d76b1 --- /dev/null +++ b/fonts/LICENSE @@ -0,0 +1,3 @@ +GnuMICR.otf is licensed under the GNU General Public License v2.0. +Source: https://github.com/alerque/gnumicr +Full license text: https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt \ No newline at end of file diff --git a/fonts/NEWS b/fonts/NEWS new file mode 100644 index 0000000..22bbdcb --- /dev/null +++ b/fonts/NEWS @@ -0,0 +1,15 @@ +Feb 2, 2004 + Hm, this version 0.30 has been sitting around for a while, put it out + there! Several people have reported using this font with good success, + even in commercial applications. + +August 12, 2000 + I have someone who is willing to test the TTF version of this font in + the real world, on a fairly large scale... we'll see how it goes! + + The TTF version was just converted by a font program, so I have no + idea if conversion errors were introduced. + +July 1, 2000 + First Public Release + diff --git a/fonts/README b/fonts/README new file mode 100644 index 0000000..f54ce7f --- /dev/null +++ b/fonts/README @@ -0,0 +1,93 @@ +GNUMICR MICR / E13-B font + +A PostScript(tm) Type 1 MICR Font released under the GPL. + +Copyright (c) 2000-2003, Eric Sandeen + +This font is released under the GNU General Public License ("GPL") +(see the file COPYING for details). + +Important details to note about the license: + +1) This font comes with NO WARRANTY. I am not responsible for any + damages or expenses resulting from its use. + +2) This font may only be distributed with the license and the source code + to the font intact. It's not exactly clear to me how the GNU GPL applies to + fonts, but in my eyes, the font file "GnuMICR.raw" is the "source code" to + this font, and the files "GnuMICR.pfa" and "GnuMICR.pfb" are the compiled + versions. if you redistribute the "compiled" version, you must also + distribute, or offer to distribute, the "source" version (see COPYING). + + Also, it is my wish that this font not be distributed in such a way + that it is built into a proprietary piece of software. I'll leave + the legal wrangling to the lawyers, but in my opinion, if you + write, say, a non-GPL'd check printing application for Windows, + you should not hard-code or embed this font into your application. + I feel that this would be the font-equivalent of linking libraries. + If you wish to distribute this font with your app, that's fine, + but I feel that it should be distributed alongside the application, + with all copyright & license info intact, per the terms of the GPL. + + I have spent MANY hours on this font. Please respect my work, and + follow my wishes regarding licensing of this particular font. + + For a brief introduction to fonts, copyright, and piracy, see + "Ethics and Licensing Issues Related to Type" at + http://www.linuxdoc.org/HOWTO/Font-HOWTO-12.html + +-------------------------- + +Ok, with that out of the way... + +A while ago, I set out to be able to print my own checks under Linux. The +first requirement was that I find a MICR font (those funky numbers and +symbols at the bottom of your check) that could be freely +distributed with the application. I quickly found that such a thing didn't +exist, so I set out to make my own. This font is the result. + +I coded this font by hand, without the aid of any GUI font application. +My goal was to be as accurate as possible, with complete control over +the resulting font. I found dimensions and other specs for the font +and their use at http://www.cdnpay.ca/eng/rules/006.ENG.htm +This seems to be mostly geared towards Canadian standards, but the MICR +font is an ISO standard, and the glyph dimensions should be the same +as are used in the United States. + +My sole source for information for this font was the URL above. This font +is NOT a modified version of any existing MICR font. + +The "source code" to this font is the file "GnuMICR.raw" This file +may be converted into an actual Type 1 (.pfa or .pfb) font with +the "t1utils" package available +at http://www.lcdf.org/~eddietwo/type/#t1utils + +I have not had this font tested by any bank. You should get best results +with a 600dpi PostScript(tm) printer, probably worse results with a 300 +dpi laserjet printer via GhostScript. The hinting on the font is likely +not perfect, so rendering at lower DPI may introduce some errors. + +I have had some very good reports of people using this font for +commercial printing, however. + +I'm not a PostScript(tm) expert by any means. If anyone who is reading +this document _is_ and would like to offer suggestions or patches for the +font or its associated .afm file, please feel free to do so. + +Also, if you have access to the tools needed to really test the font, +and you feel like doing that, I would really like to hear from you. +I'd like to make this the best MICR font available. I think it's +well on its way - see "comparison.png" for an overlay of GnuMICR +vs. a sample from a commercial font. Note the strange arcs in some +regions of the commercial font. + +The TTF font provided in this package was converted by a third +party using Windows NT. As such, I have very little confidence +in the quality of the TTF font. If you want the result of my hard +work on dimensional accuracy, use the postscript font. + +See the file INSTALL for help installing this font. + +-Eric Sandeen + July 1, 2000 + Updated March 2, 2003 diff --git a/fonts/comparison.png b/fonts/comparison.png new file mode 100644 index 0000000000000000000000000000000000000000..d7e5ea297a98b1384b52df194c2d47cbd84a3690 GIT binary patch literal 6068 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 oU@P)phuiJm=YG@BP~;)|SSsOngiL0D#rh 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 { + row[h.trim()] = values[idx] !== undefined ? values[idx] : null; + }); + rows.push(row); + } + return rows; +} + +function splitCsvLine(line) { + const result = []; + let current = ''; + let inQuotes = false; + + for (let i = 0; i < line.length; i++) { + const ch = line[i]; + if (ch === '"') { + if (inQuotes && line[i + 1] === '"') { + current += '"'; + i++; + } else { + inQuotes = !inQuotes; + } + } else if (ch === ',' && !inQuotes) { + result.push(current); + current = ''; + } else { + current += ch; + } + } + result.push(current); + return result; +} + +// ---- Font name normalization ------------------------------------------------- +// .mdb stores Windows font names; map common ones to PDFKit built-ins. +// Any unmapped font will fall back to Helvetica at render time. +const FONT_MAP = { + 'Times New Roman': 'Times-Roman', + 'Helsinki': 'Helvetica', + 'Arial': 'Helvetica', + 'Courier New': 'Courier', +}; + +function normalizeFont(fontName, isBold) { + const mapped = FONT_MAP[fontName] || 'Helvetica'; + if (isBold) { + if (mapped === 'Times-Roman') return 'Times-Bold'; + if (mapped === 'Helvetica') return 'Helvetica-Bold'; + if (mapped === 'Courier') return 'Courier-Bold'; + } + return mapped; +} + +// ---- Import: T100 (account config) ------------------------------------------ + +function importAccount() { + console.log('\n--- Importing account config (T100) ---'); + const rows = mdbExport('T100'); + + if (rows.length === 0) { + console.error('No rows in T100. Is this a valid ezCheckPrinting .mdb?'); + process.exit(1); + } + + // Take the first (and typically only) row + const r = rows[0]; + console.log(`Account: ${r.Company1} / Bank: ${r.BankName}`); + console.log(`Routing: ${r.BankRouteNo} | Account: ${r.BankAccountNo}`); + console.log(`Current check no: ${r.CurrentCheckNo}`); + + // Logo is stored as base64 in the Settings table (not T100) + // We fetch it separately below and update after insert. + const accountData = { + bank_name: r.BankName?.trim() || '', + bank_info1: r.BankInfo1?.trim() || null, + bank_info2: r.BankInfo2?.trim() || null, + bank_info3: r.BankInfo3?.trim() || null, + transit_code: r.TransitCode?.trim() || null, + routing_number: r.BankRouteNo?.trim() || '', + account_number: r.BankAccountNo?.trim() || '', + start_check_no: parseInt(r.StartCheckNo) || 1000, + current_check_no: parseInt(r.CurrentCheckNo) || 1000, + check_width: parseFloat(r.CheckWidth) || 8.5, + check_height: parseFloat(r.CheckHeight) || 3.5, + offset_left: parseFloat(r.OffsetLeft) || 0, + offset_right: parseFloat(r.OffsetRight) || 0, + offset_up: parseFloat(r.OffsetUp) || 0, + offset_down: parseFloat(r.OffsetDown) || 0, + company1: r.Company1?.trim() || null, + company2: r.Company2?.trim() || null, + company3: r.Company3?.trim() || null, + company4: r.Company4?.trim() || null, + blank_stock: r.BlankBankStock === 'true' || r.BlankBankStock === '1' ? 1 : 0, + check_position: r.ExField1?.trim() || '3-per-page', + }; + + if (!dryRun) { + // Delete existing account row (single-account Phase 1 assumption) + db.prepare('DELETE FROM account').run(); + db.prepare(` + INSERT INTO account ( + bank_name, bank_info1, bank_info2, bank_info3, transit_code, + routing_number, account_number, start_check_no, current_check_no, + check_width, check_height, offset_left, offset_right, offset_up, offset_down, + company1, company2, company3, company4, + blank_stock, check_position + ) VALUES ( + @bank_name, @bank_info1, @bank_info2, @bank_info3, @transit_code, + @routing_number, @account_number, @start_check_no, @current_check_no, + @check_width, @check_height, @offset_left, @offset_right, @offset_up, @offset_down, + @company1, @company2, @company3, @company4, + @blank_stock, @check_position + ) + `).run(accountData); + console.log('Account config imported.'); + } else { + console.log('[dry-run] Would insert:', JSON.stringify(accountData, null, 2)); + } + + return accountData; +} + +// ---- Import: Settings (logo image) ------------------------------------------ + +function importLogo() { + console.log('\n--- Importing logo from Settings table ---'); + const rows = mdbExport('Settings'); + const logoRow = rows.find(r => r.SettingKey === 'LogoImg'); + + if (!logoRow || !logoRow.SettingValue) { + console.log('No logo found in Settings table.'); + return; + } + + // Value is raw base64 (GIF format based on the data we saw) + const base64Data = logoRow.SettingValue.trim(); + const dataUri = `data:image/gif;base64,${base64Data}`; + + if (!dryRun) { + db.prepare('UPDATE account SET logo_data = ? WHERE id = 1').run(dataUri); + console.log(`Logo imported (${Math.round(base64Data.length / 1024)} KB base64).`); + } else { + console.log(`[dry-run] Would import logo (${Math.round(base64Data.length / 1024)} KB base64).`); + } +} + +// ---- Import: T200 (check layout fields) ------------------------------------- + +function importLayoutFields() { + console.log('\n--- Importing check layout fields (T200) ---'); + const rows = mdbExport('T200'); + console.log(`Found ${rows.length} layout fields.`); + + if (!dryRun) { + db.prepare('DELETE FROM layout_fields').run(); + } + + const insert = db.prepare(` + INSERT INTO layout_fields ( + field_name, field_text, font_name, font_size, font_bold, + field_type, line_thick, x_pos, y_pos, x_end_pos, y_end_pos, + visible, not_for_preprint + ) VALUES ( + @field_name, @field_text, @font_name, @font_size, @font_bold, + @field_type, @line_thick, @x_pos, @y_pos, @x_end_pos, @y_end_pos, + @visible, @not_for_preprint + ) + `); + + let count = 0; + for (const r of rows) { + const isBold = r.FldFontType === '1'; + const fieldData = { + field_name: r.FldName?.trim() || '', + field_text: r.FldText?.trim() || null, + font_name: normalizeFont(r.FldFontName?.trim(), isBold), + font_size: parseFloat(r.FldFontSize) || 10, + font_bold: isBold ? 1 : 0, + field_type: r.FldType?.trim() || 'Regular', + line_thick: parseInt(r.LnThick) || 1, + x_pos: parseFloat(r.XPos) || 0, + y_pos: parseFloat(r.YPos) || 0, + x_end_pos: parseFloat(r.XEndPos) || 0, + y_end_pos: parseFloat(r.YEndPos) || 0, + visible: r.Display === '1' ? 1 : 0, + not_for_preprint: parseInt(r.NotForPreprint) || 0, + }; + + if (!dryRun) { + insert.run(fieldData); + } else { + console.log(` [dry-run] ${fieldData.field_name}: type=${fieldData.field_type} x=${fieldData.x_pos} y=${fieldData.y_pos}`); + } + count++; + } + + console.log(`${dryRun ? '[dry-run] Would import' : 'Imported'} ${count} layout fields.`); +} + +// ---- Import: T104 (check records) ------------------------------------------- + +function importChecks() { + console.log('\n--- Importing check records (T104) ---'); + const rows = mdbExport('T104'); + console.log(`Found ${rows.length} check records.`); + + if (rows.length === 0) { + console.log('No checks to import.'); + return; + } + + if (!dryRun) { + db.prepare('DELETE FROM checks').run(); + } + + const insert = db.prepare(` + INSERT INTO checks ( + check_no, payee, amount, check_date, memo, note1, note2, + payee_address1, payee_address2, payee_address3, payee_address4, + printed, add_date, mdb_check_id + ) VALUES ( + @check_no, @payee, @amount, @check_date, @memo, @note1, @note2, + @payee_address1, @payee_address2, @payee_address3, @payee_address4, + @printed, @add_date, @mdb_check_id + ) + `); + + let count = 0; + let skipped = 0; + for (const r of rows) { + // Normalize date: .mdb uses MM/DD/YYYY or similar; convert to YYYY-MM-DD + const rawDate = r.CheckDate?.trim() || ''; + const checkDate = normalizeDate(rawDate); + + const addDate = normalizeDate(r.AddDate?.trim() || '') || new Date().toISOString(); + + const checkData = { + check_no: parseInt(r.CheckNo), + payee: r.Payee?.trim() || '', + amount: parseFloat(r.Amount) || 0, + check_date: checkDate, + memo: r.Memo?.trim() || null, + note1: r.Note1?.trim() || null, + note2: r.Note2?.trim() || null, + payee_address1: r.PayeeAddress1?.trim() || null, + payee_address2: r.PayeeAddress2?.trim() || null, + payee_address3: r.PayeeAddress3?.trim() || null, + payee_address4: r.PayeeAddress4?.trim() || null, + printed: r.checked === 'true' || r.checked === '1' ? 1 : 0, + add_date: addDate, + mdb_check_id: parseInt(r.CheckID) || null, + }; + + if (!checkData.check_no || !checkData.payee) { + console.warn(` Skipping record with missing check_no or payee:`, r); + skipped++; + continue; + } + + if (!dryRun) { + try { + insert.run(checkData); + } catch (err) { + console.warn(` Skipping duplicate check #${checkData.check_no}:`, err.message); + skipped++; + continue; + } + } else { + console.log(` [dry-run] Check #${checkData.check_no}: ${checkData.payee} $${checkData.amount} ${checkData.check_date}`); + } + count++; + } + + console.log(`${dryRun ? '[dry-run] Would import' : 'Imported'} ${count} checks.${skipped > 0 ? ` Skipped ${skipped}.` : ''}`); +} + +// ---- Date normalization ----------------------------------------------------- + +function normalizeDate(raw) { + if (!raw) return null; + // mdb-export outputs dates as "MM/DD/YYYY HH:MM:SS" or "YYYY-MM-DD" + const mdyMatch = raw.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})/); + if (mdyMatch) { + const [, m, d, y] = mdyMatch; + return `${y}-${m.padStart(2, '0')}-${d.padStart(2, '0')}`; + } + const isoMatch = raw.match(/^(\d{4}-\d{2}-\d{2})/); + if (isoMatch) return isoMatch[1]; + return raw; +} + +// ---- Run -------------------------------------------------------------------- + +console.log(`\nImporting from: ${mdbFile}`); +console.log(`Target database: ${process.env.DB_PATH || 'data/ezcheck.db'}`); + +try { + importAccount(); + importLogo(); + importLayoutFields(); + importChecks(); + console.log('\nMigration complete.'); +} catch (err) { + console.error('\nMigration failed:', err); + process.exit(1); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..232b154 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "ezcheck", + "version": "0.1.0", + "description": "Self-hosted check printing web app", + "main": "src/app.js", + "scripts": { + "start": "node src/app.js", + "dev": "nodemon src/app.js", + "migrate": "node migrations/import-mdb.js" + }, + "dependencies": { + "better-sqlite3": "^9.4.3", + "express": "^4.18.3", + "pdfkit": "^0.15.0" + }, + "devDependencies": { + "nodemon": "^3.1.0" + }, + "engines": { + "node": ">=20" + } +} diff --git a/public/css/style.css b/public/css/style.css new file mode 100644 index 0000000..4ad414c --- /dev/null +++ b/public/css/style.css @@ -0,0 +1,313 @@ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +:root { + --bg: #f5f5f5; + --surface: #ffffff; + --border: #d0d0d0; + --header-bg: #1a2b3c; + --header-fg: #ffffff; + --primary: #2563eb; + --primary-hover: #1d4ed8; + --danger: #dc2626; + --text: #1a1a1a; + --text-muted: #6b7280; + --row-hover: #f0f4ff; + --panel-width: 420px; + --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; +} + +body { + font-family: var(--font); + font-size: 13px; + background: var(--bg); + color: var(--text); + height: 100vh; + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* ── Header ── */ +header { + background: var(--header-bg); + color: var(--header-fg); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1rem; + height: 44px; + flex-shrink: 0; +} +.header-brand { font-size: 15px; font-weight: 600; } +.header-info { font-size: 12px; color: rgba(255,255,255,0.7); } +.header-info strong { color: #fff; } + +/* ── Toolbar ── */ +.toolbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 1rem; + background: var(--surface); + border-bottom: 1px solid var(--border); + flex-shrink: 0; +} +.toolbar-left { display: flex; align-items: center; gap: 8px; } +.toolbar-right { display: flex; align-items: center; gap: 8px; } +.toolbar label { font-size: 12px; font-weight: 500; color: var(--text-muted); } + +select { + border: 1px solid var(--border); + border-radius: 4px; + padding: 4px 8px; + font-size: 13px; + font-family: var(--font); + background: var(--surface); + cursor: pointer; +} + +/* ── Buttons ── */ +button { + cursor: pointer; + border: none; + border-radius: 4px; + font-size: 13px; + font-family: var(--font); + padding: 5px 12px; + line-height: 1.4; + transition: background 0.1s, opacity 0.1s; +} +button:disabled { opacity: 0.45; cursor: not-allowed; } + +.btn-primary { background: var(--primary); color: #fff; font-weight: 500; } +.btn-primary:not(:disabled):hover { background: var(--primary-hover); } + +.btn-secondary { + background: var(--surface); + color: var(--text); + border: 1px solid var(--border); +} +.btn-secondary:hover { background: var(--bg); } + +.btn-ghost { background: transparent; color: var(--text-muted); } +.btn-ghost:hover { color: var(--text); background: var(--bg); } + +.btn-icon { + background: transparent; + color: rgba(255,255,255,0.75); + font-size: 22px; + padding: 0 6px; + line-height: 1; + border-radius: 3px; +} +.btn-icon:hover { color: #fff; background: rgba(255,255,255,0.1); } + +.btn-sm { + padding: 2px 8px; + font-size: 12px; +} +.btn-edit { background: transparent; color: var(--primary); } +.btn-edit:hover { background: #eff6ff; } +.btn-delete { background: transparent; color: var(--danger); } +.btn-delete:hover { background: #fee2e2; } +.btn-reprint { background: transparent; color: var(--text-muted); } +.btn-reprint:hover { background: var(--bg); } + +.badge { + display: inline-block; + background: rgba(255,255,255,0.25); + border-radius: 10px; + padding: 1px 7px; + font-size: 11px; + min-width: 20px; + text-align: center; + font-weight: 600; +} + +/* ── Table ── */ +.table-wrap { + flex: 1; + overflow-y: auto; + background: var(--surface); +} + +table { + width: 100%; + border-collapse: collapse; +} + +thead th { + position: sticky; + top: 0; + z-index: 1; + background: #f8f8f8; + border-bottom: 2px solid var(--border); + padding: 6px 8px; + text-align: left; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--text-muted); + white-space: nowrap; + user-select: none; +} +thead th.sortable { cursor: pointer; } +thead th.sortable:hover { color: var(--text); } +thead th.sort-asc .sort-indicator::after { content: ' ↑'; } +thead th.sort-desc .sort-indicator::after { content: ' ↓'; } + +tbody tr { border-bottom: 1px solid #f0f0f0; } +tbody tr:hover { background: var(--row-hover); } +tbody tr.printed { color: var(--text-muted); } + +td { + padding: 6px 8px; + vertical-align: middle; +} + +.col-select { width: 32px; } +.col-no { width: 60px; font-variant-numeric: tabular-nums; } +.col-date { width: 95px; white-space: nowrap; } +.col-amount { width: 95px; text-align: right; font-variant-numeric: tabular-nums; font-weight: 500; } +.col-memo { color: var(--text-muted); font-size: 12px; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.col-status { width: 88px; text-align: center; } +.col-actions { width: 130px; text-align: right; white-space: nowrap; } + +.status-badge { + display: inline-block; + font-size: 10px; + font-weight: 600; + letter-spacing: 0.05em; + text-transform: uppercase; + padding: 2px 7px; + border-radius: 10px; +} +.status-unprinted { background: #fef3c7; color: #92400e; } +.status-printed { background: #e0f2fe; color: #0369a1; } + +.loading-row td, +.empty-row td { text-align: center; padding: 2rem; color: var(--text-muted); } + +/* ── Slide-in panel ── */ +#panel-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.3); + z-index: 100; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s; +} +#panel-overlay.open { opacity: 1; pointer-events: auto; } + +#check-panel { + position: fixed; + top: 0; + right: 0; + width: var(--panel-width); + height: 100vh; + background: var(--surface); + z-index: 101; + box-shadow: -4px 0 24px rgba(0,0,0,0.15); + transform: translateX(100%); + transition: transform 0.2s ease; + display: flex; + flex-direction: column; + overflow-y: auto; +} +#check-panel.open { transform: translateX(0); } + +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + background: var(--header-bg); + color: var(--header-fg); + flex-shrink: 0; + position: sticky; + top: 0; + z-index: 1; +} +.panel-header h2 { font-size: 14px; font-weight: 600; } + +/* ── Form ── */ +#check-form { + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; + flex: 1; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 4px; +} +.form-group label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted); +} +.form-group.required label::after { content: ' *'; color: var(--danger); } + +.form-group input { + border: 1px solid var(--border); + border-radius: 4px; + padding: 7px 10px; + font-size: 13px; + font-family: var(--font); + width: 100%; + background: var(--surface); + color: var(--text); +} +.form-group input:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 2px rgba(37,99,235,0.15); +} +.form-group input.error { border-color: var(--danger); } + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +.address-section { + border: 1px solid var(--border); + border-radius: 4px; +} +.address-section summary { + padding: 8px 12px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted); + cursor: pointer; + user-select: none; + list-style: none; +} +.address-section summary::before { content: '▶ '; font-size: 9px; } +.address-section[open] summary::before { content: '▼ '; } +.address-section[open] summary { border-bottom: 1px solid var(--border); } +.address-fields { + padding: 10px 12px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.form-actions { + display: flex; + gap: 8px; + padding-top: 4px; + border-top: 1px solid var(--border); + margin-top: auto; +} +.form-actions .btn-primary { flex: 1; padding: 8px; } diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..65947db --- /dev/null +++ b/public/index.html @@ -0,0 +1,122 @@ + + + + + + ezcheck + + + +
+
+ ezcheck +
+
+ Next check: +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + +
# Date Payee Amount MemoStatus
Loading…
+
+ + +
+ + + + + diff --git a/public/js/app.js b/public/js/app.js new file mode 100644 index 0000000..a1d56fd --- /dev/null +++ b/public/js/app.js @@ -0,0 +1,391 @@ +'use strict'; + +const state = { + checks: [], + account: null, + filter: '0', // '' = all, '0' = unprinted, '1' = printed + sortCol: 'check_no', + sortDir: 'desc', + selected: new Set(), + editingId: null, +}; + +// ── API helpers ────────────────────────────────────────────────────────────── + +async function apiFetch(method, path, body) { + const opts = { method, headers: { 'Content-Type': 'application/json' } }; + if (body !== undefined) opts.body = JSON.stringify(body); + const res = await fetch(path, opts); + if (res.status === 204) return null; + const data = await res.json(); + if (!res.ok) throw new Error(data.error || res.statusText); + return data; +} + +// ── Data loading ───────────────────────────────────────────────────────────── + +async function loadAccount() { + try { + state.account = await apiFetch('GET', '/api/account'); + renderHeader(); + } catch { + // account not configured yet — silently skip + } +} + +async function loadChecks() { + const tbody = document.getElementById('checks-tbody'); + tbody.innerHTML = 'Loading…'; + try { + const params = new URLSearchParams(); + if (state.filter !== '') params.set('printed', state.filter); + state.checks = await apiFetch('GET', `/api/checks?${params}`); + state.selected.clear(); + renderTable(); + refreshPdfButton(); + } catch (err) { + tbody.innerHTML = `Error loading checks: ${escHtml(err.message)}`; + } +} + +// ── Rendering ──────────────────────────────────────────────────────────────── + +function renderHeader() { + const a = state.account; + if (!a) return; + document.getElementById('company-name').textContent = a.company1 || 'ezcheck'; + document.getElementById('current-check-no').textContent = (a.current_check_no + 1).toLocaleString(); +} + +function renderTable() { + const checks = sortedChecks(); + const tbody = document.getElementById('checks-tbody'); + + if (checks.length === 0) { + tbody.innerHTML = 'No checks found.'; + updateSortIndicators(); + return; + } + + tbody.innerHTML = checks.map(renderRow).join(''); + updateSortIndicators(); + updateCheckboxStates(); + + // Attach row-level event listeners + tbody.querySelectorAll('input[type="checkbox"]').forEach(cb => { + cb.addEventListener('change', () => onCheckboxChange(cb)); + }); + tbody.querySelectorAll('.btn-edit').forEach(btn => { + btn.addEventListener('click', () => openPanel(parseInt(btn.dataset.id, 10))); + }); + tbody.querySelectorAll('.btn-delete').forEach(btn => { + btn.addEventListener('click', () => deleteCheck(parseInt(btn.dataset.id, 10))); + }); + tbody.querySelectorAll('.btn-reprint').forEach(btn => { + btn.addEventListener('click', () => reprintCheck(parseInt(btn.dataset.id, 10))); + }); +} + +function renderRow(c) { + const printed = !!c.printed; + const selected = state.selected.has(c.id); + + const fmtAmount = new Intl.NumberFormat('en-US', { + style: 'currency', currency: 'USD', + }).format(c.amount); + + const fmtDate = c.check_date + ? new Date(c.check_date + 'T12:00:00').toLocaleDateString('en-US', { + month: 'short', day: 'numeric', year: 'numeric', + }) + : '—'; + + const checkbox = printed + ? '' + : ``; + + const statusBadge = printed + ? 'Printed' + : 'Unprinted'; + + const actions = printed + ? `` + : `` + + ``; + + return ` + ${checkbox} + ${c.check_no} + ${fmtDate} + ${escHtml(c.payee)} + ${fmtAmount} + ${escHtml(c.memo || '')} + ${statusBadge} + ${actions} + `; +} + +function sortedChecks() { + const col = state.sortCol; + const dir = state.sortDir === 'asc' ? 1 : -1; + return [...state.checks].sort((a, b) => { + let av = a[col]; + let bv = b[col]; + if (col === 'amount') { av = parseFloat(av); bv = parseFloat(bv); } + if (av == null) return 1; + if (bv == null) return -1; + if (av < bv) return -dir; + if (av > bv) return dir; + return 0; + }); +} + +function updateSortIndicators() { + document.querySelectorAll('thead th.sortable').forEach(th => { + th.classList.remove('sort-asc', 'sort-desc'); + if (th.dataset.col === state.sortCol) { + th.classList.add(state.sortDir === 'asc' ? 'sort-asc' : 'sort-desc'); + } + }); +} + +function updateCheckboxStates() { + document.querySelectorAll('#checks-tbody input[type="checkbox"]').forEach(cb => { + const id = parseInt(cb.dataset.id, 10); + if (!state.selected.has(id)) { + cb.disabled = state.selected.size >= 3; + } + }); +} + +function refreshPdfButton() { + const n = state.selected.size; + const btn = document.getElementById('btn-generate-pdf'); + btn.disabled = n === 0; + document.getElementById('selected-count').textContent = n; +} + +// ── Checkbox handling ──────────────────────────────────────────────────────── + +function onCheckboxChange(cb) { + const id = parseInt(cb.dataset.id, 10); + if (cb.checked) { + if (state.selected.size >= 3) { + cb.checked = false; + return; + } + state.selected.add(id); + } else { + state.selected.delete(id); + } + refreshPdfButton(); + updateCheckboxStates(); +} + +// ── Slide-in panel ─────────────────────────────────────────────────────────── + +function openPanel(id = null) { + state.editingId = id; + const form = document.getElementById('check-form'); + const title = document.getElementById('panel-title'); + + form.reset(); + clearFormErrors(); + document.querySelector('.address-section').removeAttribute('open'); + + if (id !== null) { + const check = state.checks.find(c => c.id === id); + if (!check) return; + title.textContent = `Edit Check #${check.check_no}`; + form.payee.value = check.payee || ''; + form.amount.value = check.amount != null ? check.amount : ''; + form.check_date.value = check.check_date || ''; + form.memo.value = check.memo || ''; + form.note1.value = check.note1 || ''; + form.note2.value = check.note2 || ''; + form.payee_address1.value = check.payee_address1 || ''; + form.payee_address2.value = check.payee_address2 || ''; + form.payee_address3.value = check.payee_address3 || ''; + form.payee_address4.value = check.payee_address4 || ''; + if (check.payee_address1) { + document.querySelector('.address-section').setAttribute('open', ''); + } + } else { + title.textContent = 'New Check'; + form.check_date.value = new Date().toISOString().slice(0, 10); + } + + document.getElementById('panel-overlay').classList.add('open'); + document.getElementById('check-panel').classList.add('open'); + form.payee.focus(); +} + +function closePanel() { + document.getElementById('panel-overlay').classList.remove('open'); + document.getElementById('check-panel').classList.remove('open'); + state.editingId = null; +} + +function clearFormErrors() { + document.querySelectorAll('#check-form .error').forEach(el => el.classList.remove('error')); +} + +// ── CRUD actions ───────────────────────────────────────────────────────────── + +async function saveCheck(e) { + e.preventDefault(); + clearFormErrors(); + + const form = e.target; + const data = { + payee: form.payee.value.trim(), + amount: parseFloat(form.amount.value), + check_date: form.check_date.value, + memo: form.memo.value.trim() || null, + note1: form.note1.value.trim() || null, + note2: form.note2.value.trim() || null, + payee_address1: form.payee_address1.value.trim() || null, + payee_address2: form.payee_address2.value.trim() || null, + payee_address3: form.payee_address3.value.trim() || null, + payee_address4: form.payee_address4.value.trim() || null, + }; + + let valid = true; + if (!data.payee) { form.payee.classList.add('error'); valid = false; } + if (!data.amount || isNaN(data.amount) || data.amount <= 0) { form.amount.classList.add('error'); valid = false; } + if (!data.check_date) { form.check_date.classList.add('error'); valid = false; } + if (!valid) return; + + const btn = document.getElementById('btn-save'); + btn.disabled = true; + btn.textContent = 'Saving…'; + + try { + if (state.editingId !== null) { + await apiFetch('PUT', `/api/checks/${state.editingId}`, data); + } else { + await apiFetch('POST', '/api/checks', data); + } + closePanel(); + await Promise.all([loadAccount(), loadChecks()]); + } catch (err) { + alert(`Error: ${err.message}`); + } finally { + btn.disabled = false; + btn.textContent = 'Save Check'; + } +} + +async function deleteCheck(id) { + const check = state.checks.find(c => c.id === id); + if (!check) return; + if (!confirm(`Delete check #${check.check_no} payable to "${check.payee}"?`)) return; + try { + await apiFetch('DELETE', `/api/checks/${id}`); + await loadChecks(); + } catch (err) { + alert(`Error: ${err.message}`); + } +} + +async function generatePdf() { + const ids = [...state.selected]; + if (ids.length === 0 || ids.length > 3) return; + + const btn = document.getElementById('btn-generate-pdf'); + btn.disabled = true; + btn.textContent = 'Generating…'; + + try { + const res = await fetch('/api/pdf', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ checkIds: ids }), + }); + if (!res.ok) { + const err = await res.json().catch(() => ({ error: res.statusText })); + throw new Error(err.error || res.statusText); + } + const blob = await res.blob(); + window.open(URL.createObjectURL(blob), '_blank'); + await loadChecks(); // refresh to show printed status + } catch (err) { + alert(`PDF error: ${err.message}`); + } finally { + refreshPdfButton(); + } +} + +async function reprintCheck(id) { + const check = state.checks.find(c => c.id === id); + if (!check) return; + if (!confirm(`Reprint check #${check.check_no} to "${check.payee}"?\n(Will not re-mark as printed)`)) return; + try { + const res = await fetch('/api/pdf?mark_printed=false', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ checkIds: [id] }), + }); + if (!res.ok) { + const err = await res.json().catch(() => ({ error: res.statusText })); + throw new Error(err.error || res.statusText); + } + const blob = await res.blob(); + window.open(URL.createObjectURL(blob), '_blank'); + } catch (err) { + alert(`Reprint error: ${err.message}`); + } +} + +// ── Utilities ──────────────────────────────────────────────────────────────── + +function escHtml(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +// ── Initialization ─────────────────────────────────────────────────────────── + +function init() { + // Column sort + document.querySelectorAll('thead th.sortable').forEach(th => { + th.addEventListener('click', () => { + if (state.sortCol === th.dataset.col) { + state.sortDir = state.sortDir === 'asc' ? 'desc' : 'asc'; + } else { + state.sortCol = th.dataset.col; + state.sortDir = th.dataset.col === 'check_no' ? 'desc' : 'asc'; + } + renderTable(); + }); + }); + + // Filter dropdown + document.getElementById('filter-status').addEventListener('change', e => { + state.filter = e.target.value; + loadChecks(); + }); + + // New check + document.getElementById('btn-new-check').addEventListener('click', () => openPanel()); + + // Panel close + document.getElementById('btn-close-panel').addEventListener('click', closePanel); + document.getElementById('btn-cancel').addEventListener('click', closePanel); + document.getElementById('panel-overlay').addEventListener('click', closePanel); + + // Form submit + document.getElementById('check-form').addEventListener('submit', saveCheck); + + // Generate PDF + document.getElementById('btn-generate-pdf').addEventListener('click', generatePdf); + + // Initial data load + loadAccount(); + loadChecks(); +} + +document.addEventListener('DOMContentLoaded', init); diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..29eb999 --- /dev/null +++ b/src/app.js @@ -0,0 +1,41 @@ +'use strict'; + +const express = require('express'); +const path = require('path'); + +const app = express(); + +app.use(express.json()); +app.use(express.static(path.join(__dirname, '../public'))); + +// Routes +app.use('/api/checks', require('./routes/checks')); +app.use('/api/pdf', require('./routes/pdf')); + +// Account info endpoint (read-only for Phase 1) +app.get('/api/account', (req, res) => { + const db = require('./db/database'); + const account = db.prepare( + 'SELECT id, bank_name, bank_info1, bank_info2, bank_info3, transit_code, ' + + 'routing_number, account_number, current_check_no, ' + + 'company1, company2, company3, company4, check_position FROM account WHERE id = 1' + ).get(); + if (!account) { + return res.status(404).json({ error: 'No account configured. Run migration first.' }); + } + // Never send routing/account numbers in cleartext to the browser in production. + // For local-only Phase 1 this is acceptable; redact for any network-exposed deployment. + res.json(account); +}); + +// Catch-all: serve index.html for client-side routing +app.get('*', (req, res) => { + res.sendFile(path.join(__dirname, '../public/index.html')); +}); + +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`ezcheck running on http://localhost:${PORT}`); +}); + +module.exports = app; diff --git a/src/db/database.js b/src/db/database.js new file mode 100644 index 0000000..17a2a5a --- /dev/null +++ b/src/db/database.js @@ -0,0 +1,26 @@ +'use strict'; + +const Database = require('better-sqlite3'); +const fs = require('fs'); +const path = require('path'); + +const DB_PATH = process.env.DB_PATH || path.join(__dirname, '../../data/ezcheck.db'); +const SCHEMA_PATH = path.join(__dirname, 'schema.sql'); + +// Ensure data directory exists +const dataDir = path.dirname(DB_PATH); +if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); +} + +const db = new Database(DB_PATH); + +// Enable WAL mode for better concurrent read performance +db.pragma('journal_mode = WAL'); +db.pragma('foreign_keys = ON'); + +// Initialize schema on first run +const schema = fs.readFileSync(SCHEMA_PATH, 'utf8'); +db.exec(schema); + +module.exports = db; diff --git a/src/db/schema.sql b/src/db/schema.sql new file mode 100644 index 0000000..9fa7aed --- /dev/null +++ b/src/db/schema.sql @@ -0,0 +1,82 @@ +-- ezcheck SQLite schema +-- Mirrors .mdb structure (T100, T104, T200) with readable column names. +-- One account per database is the Phase 1 assumption. +-- Phase 2 will add foreign keys and an account switcher. + +CREATE TABLE IF NOT EXISTS account ( + id INTEGER PRIMARY KEY, + bank_name TEXT NOT NULL, + bank_info1 TEXT, + bank_info2 TEXT, + bank_info3 TEXT, + transit_code TEXT, + routing_number TEXT NOT NULL, + account_number TEXT NOT NULL, + start_check_no INTEGER NOT NULL DEFAULT 1000, + current_check_no INTEGER NOT NULL DEFAULT 1000, + check_width REAL NOT NULL DEFAULT 8.5, + check_height REAL NOT NULL DEFAULT 3.5, + -- Per-check offset adjustments in inches (for printer calibration) + offset_left REAL NOT NULL DEFAULT 0, + offset_right REAL NOT NULL DEFAULT 0, + offset_up REAL NOT NULL DEFAULT 0, + offset_down REAL NOT NULL DEFAULT 0, + -- Company info lines (printed top-left of check) + company1 TEXT, + company2 TEXT, + company3 TEXT, + company4 TEXT, + -- Images stored as base64 data URIs + logo_data TEXT, + signature_data TEXT, + -- Metadata + blank_stock INTEGER NOT NULL DEFAULT 1, -- 1 = blank check stock + check_position TEXT NOT NULL DEFAULT '3-per-page', + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS checks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + check_no INTEGER NOT NULL, + payee TEXT NOT NULL, + amount REAL NOT NULL, + check_date TEXT NOT NULL, -- ISO date string YYYY-MM-DD + memo TEXT, + note1 TEXT, + note2 TEXT, + payee_address1 TEXT, + payee_address2 TEXT, + payee_address3 TEXT, + payee_address4 TEXT, + printed INTEGER NOT NULL DEFAULT 0, -- 0 = not printed, 1 = printed + add_date TEXT NOT NULL DEFAULT (datetime('now')), + -- original .mdb CheckID preserved if migrated, null if created in app + mdb_check_id INTEGER, + UNIQUE(check_no) +); + +CREATE TABLE IF NOT EXISTS layout_fields ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + field_name TEXT NOT NULL UNIQUE, + field_text TEXT, -- static label text (for Text type fields) + font_name TEXT NOT NULL DEFAULT 'Helvetica', + font_size REAL NOT NULL DEFAULT 10, + -- FldFontType from .mdb: 0=normal, 1=bold + font_bold INTEGER NOT NULL DEFAULT 0, + -- FldType: 'Regular' (data), 'Text' (static label), 'Graph' (image), 'Line' + field_type TEXT NOT NULL DEFAULT 'Regular', + line_thick INTEGER NOT NULL DEFAULT 1, + x_pos REAL NOT NULL DEFAULT 0, + y_pos REAL NOT NULL DEFAULT 0, + x_end_pos REAL NOT NULL DEFAULT 0, + y_end_pos REAL NOT NULL DEFAULT 0, + visible INTEGER NOT NULL DEFAULT 1, + -- 1 = only used on blank stock (not preprinted). We always render these. + not_for_preprint INTEGER NOT NULL DEFAULT 0 +); + +-- Index for fast ledger queries +CREATE INDEX IF NOT EXISTS idx_checks_date ON checks(check_date); +CREATE INDEX IF NOT EXISTS idx_checks_printed ON checks(printed); +CREATE INDEX IF NOT EXISTS idx_checks_check_no ON checks(check_no); diff --git a/src/routes/checks.js b/src/routes/checks.js new file mode 100644 index 0000000..0fef4f1 --- /dev/null +++ b/src/routes/checks.js @@ -0,0 +1,139 @@ +'use strict'; + +const express = require('express'); +const router = express.Router(); +const db = require('../db/database'); + +// GET /api/checks - list all checks, newest first +router.get('/', (req, res) => { + const { after, printed } = req.query; + let query = 'SELECT * FROM checks'; + const params = []; + const conditions = []; + + if (after) { + conditions.push('check_date >= ?'); + params.push(after); + } + if (printed !== undefined) { + conditions.push('printed = ?'); + params.push(printed === 'true' || printed === '1' ? 1 : 0); + } + + if (conditions.length) { + query += ' WHERE ' + conditions.join(' AND '); + } + query += ' ORDER BY check_no DESC'; + + const checks = db.prepare(query).all(...params); + res.json(checks); +}); + +// GET /api/checks/:id +router.get('/:id', (req, res) => { + const check = db.prepare('SELECT * FROM checks WHERE id = ?').get(req.params.id); + if (!check) return res.status(404).json({ error: 'Check not found' }); + res.json(check); +}); + +// POST /api/checks - create a new check +router.post('/', (req, res) => { + const { payee, amount, check_date, memo, note1, note2, + payee_address1, payee_address2, payee_address3, payee_address4 } = req.body; + + if (!payee || !amount || !check_date) { + return res.status(400).json({ error: 'payee, amount, and check_date are required' }); + } + + // Get next check number from account + const account = db.prepare('SELECT current_check_no FROM account WHERE id = 1').get(); + if (!account) return res.status(500).json({ error: 'No account configured. Run migration first.' }); + + const checkNo = account.current_check_no + 1; + + const insertCheck = db.prepare(` + INSERT INTO checks (check_no, payee, amount, check_date, memo, note1, note2, + payee_address1, payee_address2, payee_address3, payee_address4) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + + const updateAccountCheckNo = db.prepare( + 'UPDATE account SET current_check_no = ?, updated_at = datetime(\'now\') WHERE id = 1' + ); + + const transaction = db.transaction(() => { + const result = insertCheck.run( + checkNo, payee, parseFloat(amount), check_date, + memo || null, note1 || null, note2 || null, + payee_address1 || null, payee_address2 || null, + payee_address3 || null, payee_address4 || null + ); + updateAccountCheckNo.run(checkNo); + return result.lastInsertRowid; + }); + + const newId = transaction(); + const newCheck = db.prepare('SELECT * FROM checks WHERE id = ?').get(newId); + res.status(201).json(newCheck); +}); + +// PUT /api/checks/:id - update a check +router.put('/:id', (req, res) => { + const check = db.prepare('SELECT * FROM checks WHERE id = ?').get(req.params.id); + if (!check) return res.status(404).json({ error: 'Check not found' }); + + if (check.printed) { + return res.status(409).json({ error: 'Cannot edit a check that has been printed.' }); + } + + const { payee, amount, check_date, memo, note1, note2, + payee_address1, payee_address2, payee_address3, payee_address4 } = req.body; + + db.prepare(` + UPDATE checks SET + payee = ?, amount = ?, check_date = ?, memo = ?, note1 = ?, note2 = ?, + payee_address1 = ?, payee_address2 = ?, payee_address3 = ?, payee_address4 = ? + WHERE id = ? + `).run( + payee ?? check.payee, + amount !== undefined ? parseFloat(amount) : check.amount, + check_date ?? check.check_date, + memo ?? check.memo, + note1 ?? check.note1, + note2 ?? check.note2, + payee_address1 ?? check.payee_address1, + payee_address2 ?? check.payee_address2, + payee_address3 ?? check.payee_address3, + payee_address4 ?? check.payee_address4, + req.params.id + ); + + res.json(db.prepare('SELECT * FROM checks WHERE id = ?').get(req.params.id)); +}); + +// DELETE /api/checks/:id +router.delete('/:id', (req, res) => { + const check = db.prepare('SELECT * FROM checks WHERE id = ?').get(req.params.id); + if (!check) return res.status(404).json({ error: 'Check not found' }); + + if (check.printed) { + return res.status(409).json({ error: 'Cannot delete a check that has been printed.' }); + } + + db.prepare('DELETE FROM checks WHERE id = ?').run(req.params.id); + res.status(204).send(); +}); + +// POST /api/checks/mark-printed - mark checks as printed +router.post('/mark-printed', (req, res) => { + const { ids } = req.body; + if (!Array.isArray(ids) || ids.length === 0) { + return res.status(400).json({ error: 'ids array required' }); + } + + const placeholders = ids.map(() => '?').join(','); + db.prepare(`UPDATE checks SET printed = 1 WHERE id IN (${placeholders})`).run(...ids); + res.json({ updated: ids.length }); +}); + +module.exports = router; diff --git a/src/routes/pdf.js b/src/routes/pdf.js new file mode 100644 index 0000000..e92281b --- /dev/null +++ b/src/routes/pdf.js @@ -0,0 +1,62 @@ +'use strict'; + +const express = require('express'); +const router = express.Router(); +const db = require('../db/database'); +const { generateCheckPdf } = require('../services/pdfService'); + +/** + * POST /api/pdf + * Body: { checkIds: [1, 2, 3] } -- 1 to 3 check IDs + * + * Returns a PDF with 1–3 checks in a 3-up layout. + * After successful generation, marks all checks as printed. + * + * Query param: ?mark_printed=false to suppress auto-marking (for reprints). + */ +router.post('/', async (req, res) => { + const { checkIds } = req.body; + + if (!Array.isArray(checkIds) || checkIds.length === 0 || checkIds.length > 3) { + return res.status(400).json({ error: 'checkIds must be an array of 1–3 IDs' }); + } + + // Fetch account + const account = db.prepare('SELECT * FROM account WHERE id = 1').get(); + if (!account) { + return res.status(500).json({ error: 'No account configured. Run migration first.' }); + } + + // Fetch checks in the order provided + const checks = checkIds.map(id => { + const check = db.prepare('SELECT * FROM checks WHERE id = ?').get(id); + if (!check) throw new Error(`Check ID ${id} not found`); + return check; + }); + + // Fetch layout fields (all visible fields) + const fields = db.prepare('SELECT * FROM layout_fields WHERE visible = 1').all(); + + try { + const pdfBuffer = await generateCheckPdf(account, checks, fields); + + // Mark as printed unless explicitly suppressed (e.g., reprint) + const markPrinted = req.query.mark_printed !== 'false'; + if (markPrinted) { + const placeholders = checkIds.map(() => '?').join(','); + db.prepare(`UPDATE checks SET printed = 1 WHERE id IN (${placeholders})`).run(...checkIds); + } + + res.set({ + 'Content-Type': 'application/pdf', + 'Content-Disposition': `inline; filename="checks-${checkIds.join('-')}.pdf"`, + 'Content-Length': pdfBuffer.length, + }); + res.send(pdfBuffer); + } catch (err) { + console.error('PDF generation error:', err); + res.status(500).json({ error: 'PDF generation failed', detail: err.message }); + } +}); + +module.exports = router; diff --git a/src/services/pdfService.js b/src/services/pdfService.js new file mode 100644 index 0000000..2015a7a --- /dev/null +++ b/src/services/pdfService.js @@ -0,0 +1,292 @@ +'use strict'; + +/** + * pdfService.js + * + * Generates a 3-up check PDF from 1–3 check records. + * All measurements are in points (72 pts/inch) internally; + * layout coordinates from the database are in inches and converted here. + * + * Page layout: + * - 8.5" × 11" letter page + * - Three check slots: each 8.5" wide × 3.667" tall + * - MICR line: hardcoded at Y = 3.4" from top of each slot + * + * Coordinate origin for each slot is top-left of that slot. + */ + +const PDFDocument = require('pdfkit'); +const path = require('path'); +const fs = require('fs'); + +const POINTS_PER_INCH = 72; +const PAGE_WIDTH_IN = 8.5; +const PAGE_HEIGHT_IN = 11; +const SLOT_HEIGHT_IN = PAGE_HEIGHT_IN / 3; // 3.667" +const MICR_Y_IN = SLOT_HEIGHT_IN - 0.267; // 0.267" from bottom of slot + +// MICR line format: transit symbol (⑆) and on-us symbol (⑈) in E-13B encoding. +// The GnuMICR / micrenc font maps these to specific characters. +// Standard MICR layout: [check#] ⑆[routing]⑆ [account#]⑈ +const MICR_FONT_PATH = process.env.MICR_FONT_PATH + ? path.resolve(process.env.MICR_FONT_PATH) + : path.join(__dirname, '../../fonts/micrenc.ttf'); + +// Amount in words conversion +function amountToWords(amount) { + const dollars = Math.floor(amount); + const cents = Math.round((amount - dollars) * 100); + + const ones = ['', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', + 'Eight', 'Nine', 'Ten', 'Eleven', 'Twelve', 'Thirteen', 'Fourteen', + 'Fifteen', 'Sixteen', 'Seventeen', 'Eighteen', 'Nineteen']; + const tens = ['', '', 'Twenty', 'Thirty', 'Forty', 'Fifty', + 'Sixty', 'Seventy', 'Eighty', 'Ninety']; + + function below1000(n) { + if (n === 0) return ''; + if (n < 20) return ones[n] + ' '; + if (n < 100) return tens[Math.floor(n / 10)] + (n % 10 ? '-' + ones[n % 10] : '') + ' '; + return ones[Math.floor(n / 100)] + ' Hundred ' + below1000(n % 100); + } + + function toWords(n) { + if (n === 0) return 'Zero'; + let result = ''; + if (Math.floor(n / 1000) > 0) { + result += below1000(Math.floor(n / 1000)) + 'Thousand '; + n = n % 1000; + } + result += below1000(n); + return result.trim(); + } + + const dollarWords = dollars === 0 ? 'Zero' : toWords(dollars); + const centStr = cents.toString().padStart(2, '0'); + return `${dollarWords} and ${centStr}/100`; +} + +// Format amount with ** padding (like the original software) +function formatAmountDisplay(amount) { + return `**${amount.toFixed(2)}`; +} + +// Format MICR line +// Standard check layout: [spaces][check#][transit symbol][routing][transit symbol][account][on-us symbol] +// Using micrenc.ttf character mappings: A=transit, B=amount, C=on-us, D=dash +// GnuMICR uses: 'A' for transit, 'C' for on-us +function formatMicrLine(routingNo, accountNo, checkNo) { + // Pad check number to 4+ digits + const checkPadded = checkNo.toString().padStart(4, '0'); + // Routing: 9 digits, wrapped in transit symbols (A in micrenc) + const routing = routingNo.replace(/\D/g, ''); + // Account: strip non-numeric, wrap in on-us symbols (C in micrenc) + const account = accountNo.replace(/[^0-9]/g, ''); + + // MICR format: A[routing]A [account]C [check#]A + // This is the standard US check layout + return `A${routing}A ${account}C ${checkPadded}A`; +} + +/** + * Main export: generates a PDF buffer for 1–3 checks. + * + * @param {Object} account - Account row from database + * @param {Array} checks - Array of 1–3 check rows from database + * @param {Array} fields - Layout field rows from layout_fields table + * @returns {Promise} PDF as a buffer + */ +function generateCheckPdf(account, checks, fields) { + return new Promise((resolve, reject) => { + const hasMicrFont = fs.existsSync(MICR_FONT_PATH); + if (!hasMicrFont) { + console.warn(`MICR font not found at ${MICR_FONT_PATH}. MICR line will use fallback font.`); + } + + const doc = new PDFDocument({ + size: [ + PAGE_WIDTH_IN * POINTS_PER_INCH, + PAGE_HEIGHT_IN * POINTS_PER_INCH, + ], + margins: { top: 0, bottom: 0, left: 0, right: 0 }, + autoFirstPage: true, + }); + + if (hasMicrFont) { + doc.registerFont('MICR', MICR_FONT_PATH); + } + + const buffers = []; + doc.on('data', chunk => buffers.push(chunk)); + doc.on('end', () => resolve(Buffer.concat(buffers))); + doc.on('error', reject); + + // Separate layout fields into check body vs stub fields + const bodyFields = fields.filter(f => !f.field_name.startsWith('Stub')); + const stubFields = fields.filter(f => f.field_name.startsWith('Stub')); + + // We always render 3 slots; empty slots get a blank placeholder + for (let slot = 0; slot < 3; slot++) { + const check = checks[slot] || null; + const slotOriginY = slot * SLOT_HEIGHT_IN; + + // Offset adjustments from account calibration + const offX = (account.offset_right - account.offset_left); + const offY = (account.offset_down - account.offset_up); + + // Helper: convert inches (relative to slot) to PDF points (absolute page) + const pt = (xIn, yIn) => ({ + x: (xIn + offX) * POINTS_PER_INCH, + y: (slotOriginY + yIn + offY) * POINTS_PER_INCH, + }); + + if (!check) { + // Draw a faint slot boundary line for empty slots (optional, useful for alignment) + doc.moveTo(0, slotOriginY * POINTS_PER_INCH) + .lineTo(PAGE_WIDTH_IN * POINTS_PER_INCH, slotOriginY * POINTS_PER_INCH) + .stroke('#cccccc'); + continue; + } + + // --- Render each layout field --- + for (const field of bodyFields) { + if (!field.visible) continue; + + const pos = pt(field.x_pos, field.y_pos); + + switch (field.field_type) { + case 'Line': { + const endPos = pt(field.x_end_pos, field.y_end_pos); + doc.moveTo(pos.x, pos.y) + .lineTo(endPos.x, endPos.y) + .lineWidth(field.line_thick || 1) + .stroke('#000000'); + break; + } + + case 'Graph': { + // Logo or signature image + const imgData = field.field_name === 'Logo' + ? account.logo_data + : account.signature_data; + + if (imgData) { + try { + // Data URI: strip the header, get base64 + const base64 = imgData.replace(/^data:[^;]+;base64,/, ''); + const imgBuffer = Buffer.from(base64, 'base64'); + const endPos = pt(field.x_end_pos, field.y_end_pos); + const w = Math.abs(endPos.x - pos.x); + const h = Math.abs(endPos.y - pos.y); + doc.image(imgBuffer, pos.x, pos.y, { width: w, height: h }); + } catch (err) { + console.warn(`Could not render image for field ${field.field_name}:`, err.message); + } + } + break; + } + + case 'Text': { + // Static label + const label = field.field_text || ''; + setFont(doc, field); + doc.fillColor('#000000') + .text(label, pos.x, pos.y, { lineBreak: false }); + break; + } + + case 'Regular': { + // Dynamic data - map field name to check/account data + const value = resolveFieldValue(field.field_name, check, account); + if (value !== null && value !== undefined && value !== '') { + setFont(doc, field); + doc.fillColor('#000000') + .text(String(value), pos.x, pos.y, { lineBreak: false }); + } + break; + } + } + } + + // --- MICR line --- + const micrLine = formatMicrLine(account.routing_number, account.account_number, check.check_no); + const micrPos = pt(0.3, MICR_Y_IN); + + if (hasMicrFont) { + doc.font('MICR').fontSize(12).fillColor('#000000') + .text(micrLine, micrPos.x, micrPos.y, { lineBreak: false }); + } else { + // Fallback: Courier approximation (will not scan, but useful for dev) + doc.font('Courier').fontSize(10).fillColor('#000000') + .text(micrLine, micrPos.x, micrPos.y, { lineBreak: false }); + } + + // --- Slot separator line --- + if (slot < 2) { + const lineY = (slotOriginY + SLOT_HEIGHT_IN) * POINTS_PER_INCH; + doc.moveTo(0, lineY) + .lineTo(PAGE_WIDTH_IN * POINTS_PER_INCH, lineY) + .lineWidth(0.5) + .stroke('#999999'); + } + } + + doc.end(); + }); +} + +/** + * Maps a layout field name to its runtime value from check/account data. + * Field names come from T200's FldName column. + */ +function resolveFieldValue(fieldName, check, account) { + switch (fieldName) { + case 'Payee Name': + return check.payee; + case 'Amount': + return formatAmountDisplay(check.amount); + case 'Text Amount': + return amountToWords(check.amount) + '***'; + case 'Date': + return check.check_date; + case 'Memo': + return check.memo; + case 'Check Number': + return check.check_no; + case 'Payee Address': + // Multi-line address + return [ + check.payee_address1, + check.payee_address2, + check.payee_address3, + check.payee_address4, + ].filter(Boolean).join('\n'); + case 'Company Name': + return account.company1; + case 'Company Name2': + return account.company2; + case 'Bank Information': + return [account.bank_info1, account.bank_info2, account.bank_info3] + .filter(Boolean).join('\n'); + case 'Bank Transit Code': + return account.transit_code; + default: + return null; + } +} + +/** + * Sets the PDFKit font based on a layout field's font properties. + * Falls back to Helvetica if the stored font name is not a built-in. + */ +function setFont(doc, field) { + const builtins = [ + 'Helvetica', 'Helvetica-Bold', 'Helvetica-Oblique', 'Helvetica-BoldOblique', + 'Times-Roman', 'Times-Bold', 'Times-Italic', 'Times-BoldItalic', + 'Courier', 'Courier-Bold', 'Courier-Oblique', 'Courier-BoldOblique', + ]; + const fontName = builtins.includes(field.font_name) ? field.font_name : 'Helvetica'; + doc.font(fontName).fontSize(field.font_size || 10); +} + +module.exports = { generateCheckPdf, amountToWords, formatMicrLine };