29 Commits

Author SHA1 Message Date
1a1431c85e chore: changelog and new readme 2025-05-07 13:28:46 -06:00
711880a670 fix: temporarily defaults asciiart to hosted file 2025-05-07 13:16:11 -06:00
4f043d2bd7 fix: banana 2025-05-07 13:07:47 -06:00
43d5916e52 fix: field rename preview 2025-05-07 12:55:19 -06:00
51aaf27fda new version 2025-05-07 10:29:15 -06:00
d5b9ef8f04 cl
All checks were successful
Create Version Tag / version-check (push) Successful in 19s
Create Version Tag / build-release (push) Has been skipped
Create Version Tag / publish-release (push) Has been skipped
2025-05-07 09:53:02 -06:00
22487224f2 fetch depth
All checks were successful
Create Version Tag / version-check (push) Successful in 20s
Create Version Tag / build-release (push) Successful in 2m25s
Create Version Tag / publish-release (push) Successful in 30s
2025-05-07 00:32:25 -06:00
6cc772bbf2 asdf
Some checks failed
Create Version Tag / version-check (push) Successful in 19s
Create Version Tag / build-release (push) Failing after 18s
Create Version Tag / publish-release (push) Has been skipped
2025-05-06 23:45:52 -06:00
430130cdaf asdf
Some checks failed
Create Version Tag / version-check (push) Failing after 18s
Create Version Tag / build-release (push) Has been skipped
Create Version Tag / publish-release (push) Has been skipped
2025-05-06 23:40:23 -06:00
9b11f14c84 fix ci
Some checks failed
Create Version Tag / version-check (push) Failing after 18s
Create Version Tag / build-release (push) Has been skipped
Create Version Tag / publish-release (push) Has been skipped
2025-05-06 23:25:15 -06:00
25378d2d3c unified ci workflow
Some checks failed
Create Version Tag / version-check (push) Successful in 20s
Create Version Tag / build-release (push) Has been skipped
Create Version Tag / publish-release (push) Failing after 17s
2025-05-06 23:13:53 -06:00
237d4c4349 whatever
All checks were successful
Create Version Tag / publish (push) Successful in 19s
2025-05-06 22:51:24 -06:00
b0fe668133 chore: trigger ci
Some checks failed
Create Version Tag / publish (push) Failing after 17s
2025-05-06 22:49:13 -06:00
7ee7d5f291 Merge pull request 'please for the love of god' (#7) from dev into main
Some checks failed
Create Version Tag / publish (push) Failing after 17s
Reviewed-on: #7
2025-05-06 21:11:36 -07:00
37f7a58f96 please for the love of god 2025-05-06 22:10:56 -06:00
4f4aee6a3e Merge pull request 'I'm tired boss' (#6) from dev into main
Some checks failed
Create Version Tag / publish (push) Failing after 14s
Reviewed-on: #6
2025-05-06 20:00:46 -07:00
4691ddc745 I'm tired boss 2025-05-06 21:00:16 -06:00
fa44985594 Merge pull request 'idek' (#5) from dev into main
Some checks failed
Create Version Tag / publish (push) Failing after 8s
Reviewed-on: #5
2025-05-06 19:17:38 -07:00
53cb40ebe8 idek 2025-05-06 20:16:56 -06:00
2113f930a7 Merge pull request 'asdf' (#4) from dev into main
Some checks failed
Create Version Tag / publish (push) Failing after 7s
Reviewed-on: #4
2025-05-06 18:54:14 -07:00
680aae8b4f asdf 2025-05-06 19:52:08 -06:00
969de4aab7 Merge pull request 'again' (#3) from dev into main
Some checks failed
Create Version Tag / publish (push) Failing after 8s
Reviewed-on: #3
2025-05-06 18:47:59 -07:00
673424d755 again 2025-05-06 19:47:18 -06:00
593f853143 Merge pull request 'add tokens' (#2) from dev into main
Some checks failed
Create Version Tag / publish (push) Failing after 7s
Reviewed-on: #2
2025-05-06 18:42:18 -07:00
490b948576 add tokens 2025-05-06 19:40:49 -06:00
7972e679ab Merge pull request 'dev' (#1) from dev into main
Some checks failed
Create Version Tag / publish (push) Failing after 7s
Reviewed-on: #1
2025-05-06 18:24:49 -07:00
91eb569d4b bump version 2025-05-06 19:24:21 -06:00
d1072d8a81 setup ci 2025-05-06 19:22:25 -06:00
6346b28581 v1 ready for publish 2025-05-06 17:53:17 -06:00
15 changed files with 204 additions and 46 deletions

View File

@@ -0,0 +1,48 @@
name: Create Version Tag
on:
push:
branches:
- main
- "prerelease-*"
jobs:
version-check:
runs-on: ubuntu-latest
outputs:
tag_created: ${{ steps.tag.outputs.tag_created }}
tag_name: ${{ steps.tag.outputs.tag_name }}
steps:
- uses: actions/checkout@v4
- name: Run version check
id: tag
uses: https://git.cyborggrizzly.com/bearmetal/ci-actions/version-check@v1
build-release:
if: needs.version-check.outputs.tag_created == 'true'
needs: version-check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build and release binaries
uses: https://git.cyborggrizzly.com/bearmetal/ci-actions/deno-release@main
with:
entrypoint: main.ts
compile-flags: "--allow-read --allow-write --allow-env --allow-net"
env:
GITEA_TOKEN: ${{ secrets.GIT_PAT }}
publish-release:
needs: build-release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
deno-version: 2.3.1
- name: Publish to JSR
run: deno publish --token ${{ secrets.JSR_PAT }}

35
CHANGELOG.md Normal file
View File

@@ -0,0 +1,35 @@
# Changelog
## v1.0.1 (2025-07-25)
<!-- auto-changelog -->
### Known Issues
- help flags can cause issues
## v1.0.0 (2025-07-25)
### Features
- Check Code Tool
- reads supplied code files to see if form fields are present and represented
in a switch statement
- Field Rename Tool
- provide a search and replace pattern to bulk rename form fields
- List Form Fields
- Sometimes you just need to see what fields there are
### Known Issues
- Field rename does not represent full change applied to the field, only the
replaced text
- help flags can cause issues
- ascii art is broken (sad face)
- banana doesn't work
## v0.0.0 (never)
this is just here for a reference to the auto-changelog
<!-- auto-changelog -->

View File

@@ -1,7 +1,16 @@
# Emma's Simple Form Field Checker # BearMetal PDF Tools
Compares a PDF form to a list of CS class files to see if all field names are A collection of tools for working with PDF forms.
present, excluding signature fields.
## Features
- Check Code Tool
- reads supplied code files to see if form fields are present and represented
in a switch statement
- Field Rename Tool
- provide a search and replace pattern to bulk rename form fields
- List Form Fields
- Sometimes you just need to see what fields there are
## Prereqs ## Prereqs
@@ -20,17 +29,51 @@ Deno >=2.2 (not required if downloading .exe)
> If you want it to be a global command, create an executables directory and add > If you want it to be a global command, create an executables directory and add
> it to the PATH > it to the PATH
### Precompiled
Download the latest release from the
[releases page](https://git.cyborggrizzly.com/BearMetal/pdf-tools/releases)
## Usage ## Usage
`checkfields <path to PDF> <comma-separated list of paths to CS class files>` `pdf-tools <tool> <args>` -> `<tool>` is one of the following
-OR- `checkfields` and follow prompts.
### Output - check-code
- field-rename
- list-form-fields
> All form fields present! ## Contributing
-OR- Contributions are welcome!
> The following field names are not present in the CS code ## License
> \<list of missing form fields\> GPL 3.0
---
### About this project
BearMetal PDF Tools is a collection of tools made to fix the current state of
PDF form editing. Adobe Acrobat is a great tool, but it's not always the easiest
to use, nor is it free. It also lacks some features that I find useful, such as
bulk renaming of form fields. There's also a lack of powerful, free, and open
source tools for PDF editing.
This project aims to fill that gap by providing a set of tools that can be used
to edit PDF forms. The tools are written in Deno, a modern and secure runtime
for JavaScript and TypeScript. They are designed to be easy to use and to
provide a great user experience.
The tools are designed to be used in a terminal, and are not designed to be
integrated into other applications. They are also not designed to be used as a
library, but rather as a command line tool.
### About BearMetal
BearMetal is a project that aims to decrapify modern web development. It is a
collection of tools, libraries, and resources that aim to make web development
more accessible and less intimidating. The project is open source and free to
use, and is designed to be used by anyone, regardless of skill level or
experience. You can find a list of libraries and tools on
[JSR](https://jsr.io/@bearmetal).

View File

@@ -5,6 +5,14 @@ import { colorize } from "./style.ts";
import { selectMenuInteractive } from "./selectMenu.ts"; import { selectMenuInteractive } from "./selectMenu.ts";
import { TerminalBlock, TerminalLayout } from "./TerminalLayout.ts"; import { TerminalBlock, TerminalLayout } from "./TerminalLayout.ts";
import { cliAlert, cliLog } from "./prompts.ts"; import { cliAlert, cliLog } from "./prompts.ts";
import type { ITool } from "../types.ts";
// Register tools here (filename, no extension)
const toolRegistry: [string, Promise<{ default: ITool }>][] = [
["checkCode", import("../tools/checkCode.ts")],
["fieldRename", import("../tools/fieldRename.ts")],
["listFormFields", import("../tools/listFormFields.ts")],
];
export class PdfToolsCli { export class PdfToolsCli {
private tools: Map<string, ITool> = new Map(); private tools: Map<string, ITool> = new Map();
@@ -13,30 +21,28 @@ export class PdfToolsCli {
private args = ArgParser.parse(Deno.args).setFlagDefs({ private args = ArgParser.parse(Deno.args).setFlagDefs({
help: ["-h", "--help"], help: ["-h", "--help"],
banana: ["-b", "--banana"],
}); });
async importTools(tools?: string) { async importTools() {
tools = tools?.replace(/\/$/, "").replace(/^\.?\//, "") || "tools"; for (const [name, toolfile] of toolRegistry) {
for (const toolfile of Deno.readDirSync(tools)) { const t = await toolfile;
if (toolfile.isFile) { try {
const tool = await import( if (t.default) {
Deno.cwd() + "/" + tools + "/" + toolfile.name
);
if (tool.default) {
this.tools.set( this.tools.set(
toCase(toolfile.name.replace(".ts", ""), "title"), toCase(name, "title"),
tool.default, t.default,
); );
} }
} catch (e) {
cliLog(e + "\n", this.terminalLayout.getBlock("body"));
} }
} }
} }
private async banana() { private async banana() {
const asciiArt = await getAsciiArt("banana"); const asciiArt = await getAsciiArt("banana");
const body = this.terminalLayout.getBlock("body"); this.closeMessage = colorize(asciiArt, "yellow");
body.clearAll();
cliLog(colorize(asciiArt, "yellow"), body);
} }
private async help() { private async help() {
@@ -50,28 +56,36 @@ export class PdfToolsCli {
public async run() { public async run() {
try { try {
await this.importTools();
const titleBlock = new TerminalBlock(); const titleBlock = new TerminalBlock();
this.terminalLayout.register("title", titleBlock); this.terminalLayout.register("title", titleBlock);
const bodyBlock = new TerminalBlock(); const bodyBlock = new TerminalBlock();
this.terminalLayout.register("body", bodyBlock); this.terminalLayout.register("body", bodyBlock);
if (this.args.getFlag("banana")) {
titleBlock.setFixedHeight(0);
await this.banana();
return;
}
if (this.args.getFlag("help") && !this.args.task) { if (this.args.getFlag("help") && !this.args.task) {
await this.help(); await this.help();
return; return;
} else if (this.args.nonFlags.length === 0 || !this.args.task) { } else if (this.args.nonFlags.length === 0 || !this.args.task) {
this.embiggenHeader(); this.embiggenHeader();
await this.importTools();
await this.toolMenu(); await this.toolMenu();
} else { } else {
await this.importTools();
const task = this.args.task; const task = this.args.task;
await this.runTool(toCase(task, "title")); await this.runTool(toCase(task, "title"));
} }
} finally { } finally {
this.cleanup();
}
}
private cleanup() {
this.terminalLayout.clearAll(); this.terminalLayout.clearAll();
Deno.stdin.setRaw(false); Deno.stdin.setRaw(false);
if (this.closeMessage) console.log(this.closeMessage); if (this.closeMessage) console.log(this.closeMessage);
} }
}
private async toolMenu() { private async toolMenu() {
const tools = this.tools.keys().toArray(); const tools = this.tools.keys().toArray();

View File

@@ -1,3 +1,4 @@
import type { callback } from "../types.ts";
import { colorize } from "./style.ts"; import { colorize } from "./style.ts";
import { TerminalBlock } from "./TerminalLayout.ts"; import { TerminalBlock } from "./TerminalLayout.ts";

View File

@@ -1,6 +1,7 @@
{ {
"name": "@bearmetal/pdf-tools", "name": "@bearmetal/pdf-tools",
"version": "0.0.1", "version": "1.0.1",
"license": "GPL 3.0",
"tasks": { "tasks": {
"dev": "deno run -A --env-file=.env --watch main.ts", "dev": "deno run -A --env-file=.env --watch main.ts",
"compile": "deno compile -o compare-form-fields.exe --target x86_64-pc-windows-msvc -R ./main.ts", "compile": "deno compile -o compare-form-fields.exe --target x86_64-pc-windows-msvc -R ./main.ts",
@@ -9,6 +10,7 @@
}, },
"imports": { "imports": {
"@std/assert": "jsr:@std/assert@1", "@std/assert": "jsr:@std/assert@1",
"@std/path": "jsr:@std/path@^1.0.9",
"pdf-lib": "npm:pdf-lib@^1.17.1", "pdf-lib": "npm:pdf-lib@^1.17.1",
"util/": "./util/" "util/": "./util/"
}, },

7
deno.lock generated
View File

@@ -1,8 +1,9 @@
{ {
"version": "4", "version": "5",
"specifiers": { "specifiers": {
"jsr:@std/assert@1": "1.0.12", "jsr:@std/assert@1": "1.0.12",
"jsr:@std/internal@^1.0.6": "1.0.6", "jsr:@std/internal@^1.0.6": "1.0.6",
"jsr:@std/path@^1.0.9": "1.0.9",
"npm:pdf-lib@^1.17.1": "1.17.1" "npm:pdf-lib@^1.17.1": "1.17.1"
}, },
"jsr": { "jsr": {
@@ -14,6 +15,9 @@
}, },
"@std/internal@1.0.6": { "@std/internal@1.0.6": {
"integrity": "9533b128f230f73bd209408bb07a4b12f8d4255ab2a4d22a1fd6d87304aca9a4" "integrity": "9533b128f230f73bd209408bb07a4b12f8d4255ab2a4d22a1fd6d87304aca9a4"
},
"@std/path@1.0.9": {
"integrity": "260a49f11edd3db93dd38350bf9cd1b4d1366afa98e81b86167b4e3dd750129e"
} }
}, },
"npm": { "npm": {
@@ -48,6 +52,7 @@
"workspace": { "workspace": {
"dependencies": [ "dependencies": [
"jsr:@std/assert@1", "jsr:@std/assert@1",
"jsr:@std/path@^1.0.9",
"npm:pdf-lib@^1.17.1" "npm:pdf-lib@^1.17.1"
] ]
} }

View File

@@ -1,3 +1,4 @@
/// <reference types="./types.ts" />
import { PdfToolsCli } from "./cli/index.ts"; import { PdfToolsCli } from "./cli/index.ts";
const app = new PdfToolsCli(); const app = new PdfToolsCli();

View File

@@ -2,6 +2,7 @@ import { forceArgs } from "../cli/forceArgs.ts";
import { cliAlert, cliLog } from "../cli/prompts.ts"; import { cliAlert, cliLog } from "../cli/prompts.ts";
import { colorize } from "../cli/style.ts"; import { colorize } from "../cli/style.ts";
import type { TerminalBlock } from "../cli/TerminalLayout.ts"; import type { TerminalBlock } from "../cli/TerminalLayout.ts";
import type { ITool } from "../types.ts";
import { loadPdfForm } from "../util/saveLoadPdf.ts"; import { loadPdfForm } from "../util/saveLoadPdf.ts";
function getCaseSyntaxPatternByFileExtension( function getCaseSyntaxPatternByFileExtension(
@@ -33,7 +34,7 @@ class CheckCode implements ITool {
[pdfPath, codePaths] = await forceArgs([pdfPath, codePaths], [ [pdfPath, codePaths] = await forceArgs([pdfPath, codePaths], [
"Please provide path to PDF file:", "Please provide path to PDF file:",
"Please provide path(s) to code file(s) (comma separated for multiple):", "Please provide path(s) to code file(s) (comma separated for multiple):",
]); ], this.block);
const form = await loadPdfForm(pdfPath); const form = await loadPdfForm(pdfPath);

View File

@@ -6,6 +6,7 @@ import { forceArgs } from "../cli/forceArgs.ts";
import { colorize } from "../cli/style.ts"; import { colorize } from "../cli/style.ts";
import { cliAlert, cliLog, cliPrompt } from "../cli/prompts.ts"; import { cliAlert, cliLog, cliPrompt } from "../cli/prompts.ts";
import { multiSelectMenuInteractive } from "../cli/selectMenu.ts"; import { multiSelectMenuInteractive } from "../cli/selectMenu.ts";
import type { callback, ITool } from "../types.ts";
async function renameFields( async function renameFields(
path: string, path: string,
@@ -122,8 +123,9 @@ class RenameFields implements ITool {
const match = patternRegex.exec(name); const match = patternRegex.exec(name);
if (match) { if (match) {
const toChange = evaluateChange(change, match); const toChange = evaluateChange(change, match);
const preview = name.replace(new RegExp(patternRegex), toChange);
foundUpdates.push([ foundUpdates.push([
`${colorize(name, "red")} -> ${colorize(toChange, "green")}`, `${colorize(name, "red")} -> ${colorize(preview, "green")}`,
() => { () => {
applyRename(field, name, patternRegex, toChange); applyRename(field, name, patternRegex, toChange);
}, },

View File

@@ -2,6 +2,7 @@ import { forceArgs } from "../cli/forceArgs.ts";
import { cliAlert } from "../cli/prompts.ts"; import { cliAlert } from "../cli/prompts.ts";
import { TerminalBlock } from "../cli/TerminalLayout.ts"; import { TerminalBlock } from "../cli/TerminalLayout.ts";
import { loadPdfForm } from "util/saveLoadPdf.ts"; import { loadPdfForm } from "util/saveLoadPdf.ts";
import type { ITool } from "../types.ts";
export class ListFormFields implements ITool { export class ListFormFields implements ITool {
name = "listformfields"; name = "listformfields";

View File

@@ -1,8 +1,7 @@
import type { TerminalBlock } from "./cli/TerminalLayout.ts"; import type { TerminalBlock } from "./cli/TerminalLayout.ts";
declare global { export type ToolFunc<T extends unknown[]> = (...args: T) => Promise<void>;
type ToolFunc<T extends unknown[]> = (...args: T) => Promise<void>; export interface ITool {
interface ITool {
name: string; name: string;
description: string; description: string;
run: ToolFunc<any[]>; run: ToolFunc<any[]>;
@@ -11,5 +10,4 @@ declare global {
setBlock?: (block: TerminalBlock) => void; setBlock?: (block: TerminalBlock) => void;
} }
type callback = (...args: any[]) => any; export type callback = (...args: any[]) => any;
}

View File

@@ -26,5 +26,5 @@ function getBearmetalAsciiPath() {
return filename.name; return filename.name;
} }
} }
return null; return "https://git.cyborggrizzly.com/BearMetal/pdf-tools/raw/branch/main/asciiart.txt";
} }

View File

@@ -1,3 +1,5 @@
import type { ToolFunc } from "../types.ts";
type transformer = (arg: string) => any; type transformer = (arg: string) => any;
interface IConfig { interface IConfig {
multiTransform?: boolean; multiTransform?: boolean;

View File

@@ -1,4 +1,4 @@
import { PDFDocument } from "pdf-lib"; import { PDFDocument, PDFTextField } from "pdf-lib";
export async function loadPdfForm(path: string) { export async function loadPdfForm(path: string) {
const pdfDoc = await loadPdf(path); const pdfDoc = await loadPdf(path);
@@ -13,6 +13,11 @@ export async function loadPdf(path: string) {
} }
export async function savePdf(doc: PDFDocument, path: string) { export async function savePdf(doc: PDFDocument, path: string) {
doc.getForm().getFields().forEach((field) => {
if (field instanceof PDFTextField) {
field.disableRichFormatting();
}
});
const pdfBytes = await doc.save(); const pdfBytes = await doc.save();
if (Deno.env.get("DRYRUN") || path.includes("dryrun")) return; if (Deno.env.get("DRYRUN") || path.includes("dryrun")) return;
await Deno.writeFile(path, pdfBytes); await Deno.writeFile(path, pdfBytes);