ROCK-9041 Fix storage-provider stream handling across plugins (audit findings) - #323
Conversation
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Update the affected plugin documentation to describe storage-provider compatibility and buffering behavior.
Review effort: Lite
Findings: 3
Open (3)
What changed in this PR
Remediates storage-provider stream handling across plugins by buffering content and disposing provider streams safely.
Changes:
- Supports non-Database providers across PDF, image, certificate, and SignNow workflows.
- Fixes Connection Cards persistence and stream positioning.
- Buffers inputs before handing them to downstream processors.
| File | Summary |
|---|---|
Plugins/org.secc.SignNowWorkflow/Workflows/SignNowCreate.cs |
Disposes provider streams after temporary-file copying. |
Plugins/org.secc.Security/org_secc/Security/WellrightRedirect.ascx.cs |
Safely reads and disposes certificate streams. |
Plugins/org.secc.Security/org_secc/Security/SignNowTest.ascx.cs |
Disposes streams after temporary-file creation. |
Plugins/org.secc.SafetyAndSecurity/Workflows/VolunteerApplicationMerge.cs |
Buffers and disposes PDF template streams. |
Plugins/org.secc.SafetyAndSecurity/Workflows/MinorVolunteerApplicationMerge.cs |
Buffers and disposes PDF template streams. |
Plugins/org.secc.SafetyAndSecurity/Workflows/MedicalIncidentReportMerge.cs |
Buffers PDF templates before processing. |
Plugins/org.secc.SafetyAndSecurity/Workflows/ExternalChurchReferenceMerge.cs |
Buffers and disposes PDF template streams. |
Plugins/org.secc.SafetyAndSecurity/Workflows/DigitalIncidentReportMerge.cs |
Buffers PDF templates before processing. |
Plugins/org.secc.PDF/Workflows/PDFFormMerge.cs |
Buffers template content before merging. |
Plugins/org.secc.PDF/Workflows/PDFCombine.cs |
Buffers PDF inputs before iText processing. |
Plugins/org.secc.LeagueApps/Utilities/APIClient.cs |
Safely reads and disposes certificate content. |
Plugins/org.secc.Imaging/AI/FaceCrop.cs |
Disposes source photo streams after cropping. |
Plugins/org.secc.ConnectionCards/Utilities/ConnectionCardsUtilties.cs |
Provides provider-safe PDF/image processing and persistence. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
jwakefield-secc
left a comment
There was a problem hiding this comment.
Review notes
Verified ReadContentBytes against Rock 1.16.12.1 BinaryFile.SaveHook and the Database, FileSystem and Azure providers. The helper and the ContentStream writes are correct. Also checked upstream: Rock develop (as of 2026-09-14) and 1.17.0.33 still have the undisposed AzureBlobStorage.Upload and the same ContentStream cache, so secc/Rock#18 and this helper stay necessary on every upgrade.
Overall: safe to merge once rebased on #321. The only code item worth adding before merge is the cell-skip validation. Everything else is low or nit.
Merge order (do first) — Medium
SignNowCreate.cs
Problem: #321 (still open) now has commit 3250b07 that also rewrites this file's temp handling, but it still reads viarenderedPDF.ContentStream.CopyTo.git merge-treeof the two heads reports a three-way conflict onSignNowCreate.csonly. Resolving it by taking #321's side drops theReadContentBytesread, the exact pattern this PR removes.
Fix: Rebase onto #321 and keep this PR's body (helper read plustry/finally) as the resolution.
New in this PR
-
ConnectionCardsUtilties.cs:117— Medium (low likelihood, data loss when it hits)
Problem: The newcontinuesilently skips cells.nbCols/nbRowsareNumberUpDowncontrols with no Minimum or Maximum, so a value large enough to make a cell 4 px or smaller skips every cell,ChopImagereturns an empty list, the block deletes the source scan, shows success, and launches no workflows. The old loop at least failed loudly withOutOfMemoryException. A value of 0 throwsDivideByZeroExceptionatsourceBitmap.Width / cols(pre-existing).
Fix: Validatecols/rows >= 1andelementWidth/elementHeight > 4up front and throw with a message. -
BinaryFileExtensions.cs:46— Low (theoretical, no caller hits it)
Problem: The helper always reads fromStorageProviderwhen one exists, so a tracked file with a reassigned but unsavedContentStreamreturns the old stored bytes. The remarks only cover the unsaved-file case.
Fix: Document the constraint (save before reading) in the remarks and README. -
ConnectionCardsUtilties.cs:113— Nit
Problem:Rectangle.Intersectcan never clip.elementWidth = Width / cols(integer), so a cell's far edge is at most(col+1) * elementWidth <= Width. Only the<= 0guard does work.
Fix: Replace with the up-front size validation above and build the rectangle directly. -
ConnectionCardsUtilties.cs:58, 83, 131— Nit
Problem:FileSize = data.Lengthis overwritten by the save hook from the provider's reported size on both the Added and Modified paths.
Fix: Drop the assignments.
Pre-existing, in a function this PR rewrote
ConnectionCardsUtilties.cs:65— Low (error page on a corrupt upload, no data loss, same as today)
Problem:ConvertPDFToImagestill returnsnew BinaryFile()(no MimeType, no content) when Ghostscript reports zero pages. The block only null-checks, adds it, and Rock's save hook throws onEntity.MimeType.StartsWith(...)(no null guard atBinaryFile.SaveHook.cs:61).
Fix: Returnnullor throw a namedInvalidOperationExceptionso the caller's null check works.
331021f to
423aac4
Compare
|
Thanks, @jwakefield-secc. All addressed, and the branch is force-pushed to Merge order / ChopImage cell skip (Medium): fixed in ebfbd52. Before anything is saved,
Helper remark (Low): documented in the XML remarks and the DevLib README: save before calling Zero-page PDF (Low): FileSize (nit): I kept these. On the Modified path the hook does overwrite unconditionally ( secc/Rock#18: agreed, and thanks for checking upstream. The PR description now says it's cherry-picked into our Rock build until upstream fixes it, and that this PR's helper complements it rather than replacing it. |
ConvertPDFToImage and ChopImage wrote content only to DatabaseData, so under any non-Database provider no content was ever saved, and ChopImage read DatabaseData.Content directly (NullReferenceException for files in Azure). All three utilities now read through ContentStream into memory, dispose the provider stream before the caller deletes the source file, and assign a fresh MemoryStream to ContentStream so the provider gets the content. RotateImage also stops handing the provider a stream positioned at its end, which uploaded a zero-byte blob under Azure. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Read the template bytes into memory and dispose the storage provider stream instead of handing iText a provider stream it never closes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Reads a BinaryFile's bytes through a fresh storage-provider stream and disposes it, leaving the entity's cached ContentStream untouched. Disposing ContentStream directly breaks later reads of the same tracked BinaryFile on Azure: the getter only re-fetches when CanSeek is false, and the Azure blob stream still reports CanSeek after Dispose. Null or empty content throws a named InvalidOperationException. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…yAndSecurity, Imaging, LeagueApps, SignNowTest Replaces the using-around-ContentStream blocks, which left a disposed stream cached on the entity under Azure. Missing or empty files now fail with a named error everywhere instead of a NullReferenceException in some paths. LeagueApps no longer returns a null certificate and re-queries on every access. SignNowTest writes with File.WriteAllBytes, which truncates, so a leftover temp file can't leave trailing bytes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…via ReadContentBytes ChopImage now iterates exactly cols x rows cells and clamps each inset rectangle to the bitmap. The old step-until-edge loop added a partial column/row when the size wasn't evenly divisible, and Clone threw OutOfMemoryException. All bitmaps are disposed. Drops the private ReadAllBytes helper and corrects the RotateImage comment (the old code leaked the provider stream and Image; it did not upload a zero-byte blob). README updated for provider-agnostic reads and the save-hook resize caveat. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Keeps #321's temp-directory handling (finally cleanup, SDK error handling) and only replaces the ContentStream.CopyTo read with the DevLib helper, so the cached stream on the tracked BinaryFile is never disposed or left open. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ia ReadContentBytes The PFX was imported with PersistKeySet on every page load and never disposed, leaving a new private-key file in the machine key store each time. It now imports without PersistKeySet and disposes the certificate after signing. Exportable stays because CertificateUtility exports the key. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rity READMEs Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s instead of losing the scan ChopImage silently skipped cells when rows/columns made a cell 4px or smaller, so the block deleted the source scan, showed success and launched no workflows (the Rows/Columns NumberUpDowns had no bounds). It now throws InvalidOperationException for fewer than 1 row/column or cells of 4px or less, before anything is saved. The block shows the message and keeps the scan. Rows/Columns get Minimum="1". The no-op Rectangle.Intersect is removed. ConvertPDFToImage returns null for a PDF with no pages (an empty BinaryFile made the save hook throw on its null MimeType), and the block shows an error. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…fore reading) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…dling Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
423aac4 to
3c922d6
Compare
* ROCK-9041 Stop SignNowDownload leaking the temp PDF file handle (#321) * ROCK-9041: Stop SignNowDownload leaking the temp PDF file handle Read the downloaded SignNow PDF into a MemoryStream instead of assigning an open FileStream to BinaryFile.ContentStream. The Azure provider did not dispose the stream on save, so the temp file stayed locked and the File.Delete afterwards threw. Using a MemoryStream removes the dependency on the provider's dispose behavior entirely. Also persist the updated content when the BinaryFile already exists (the else branch assigned a stream but never called SaveChanges), and delete the actual downloaded path ({name}.pdf) rather than {name}, which left temp PDFs behind on every pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ROCK-9041 Harden SignNowDownload temp-file handling per review Download into a per-call temp directory, check the SDK result, and delete the directory before any database work so concurrent runs can't store each other's PDF and cleanup can't throw after the save. Fall back to the Default file type so new files keep their content, return errors instead of throwing on SignNow error responses, add the .pdf extension, and remove dead code. Update the README to match. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ROCK-9041 Handle SignNowSDK error results and clean up SignNowCreate temp files RestSharp 105 never throws, so the SDK returns null or the raw error body. Treat Document.Get/Download/Create results as untyped, report a clear message for null, and catch non-JSON bodies. Reject downloads that don't start with %PDF-, since the SDK saves its second response without a status check. Guard the temp directory delete, reset MimeType on the existing-file branch, and move SignNowCreate's temp cleanup into a finally. Update the README to match. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> * ROCK-9041 Fix storage-provider stream handling across plugins (audit findings) (#323) * ROCK-9041: Make ConnectionCards utilities storage-provider agnostic ConvertPDFToImage and ChopImage wrote content only to DatabaseData, so under any non-Database provider no content was ever saved, and ChopImage read DatabaseData.Content directly (NullReferenceException for files in Azure). All three utilities now read through ContentStream into memory, dispose the provider stream before the caller deletes the source file, and assign a fresh MemoryStream to ContentStream so the provider gets the content. RotateImage also stops handing the provider a stream positioned at its end, which uploaded a zero-byte blob under Azure. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ROCK-9041: Dispose provider stream in FaceCrop Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ROCK-9041: Dispose provider stream when loading LeagueApps certificate Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ROCK-9041: Dispose provider streams in PDFCombine and PDFFormMerge Read the template bytes into memory and dispose the storage provider stream instead of handing iText a provider stream it never closes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ROCK-9041: Dispose provider streams in SafetyAndSecurity PDF merges Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ROCK-9041: Dispose provider streams in SignNowTest and WellrightRedirect Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ROCK-9041: Add BinaryFile.ReadContentBytes to DevLib Reads a BinaryFile's bytes through a fresh storage-provider stream and disposes it, leaving the entity's cached ContentStream untouched. Disposing ContentStream directly breaks later reads of the same tracked BinaryFile on Azure: the getter only re-fetches when CanSeek is false, and the Azure blob stream still reports CanSeek after Dispose. Null or empty content throws a named InvalidOperationException. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ROCK-9041: Read BinaryFile content via ReadContentBytes in PDF, SafetyAndSecurity, Imaging, LeagueApps, SignNowTest Replaces the using-around-ContentStream blocks, which left a disposed stream cached on the entity under Azure. Missing or empty files now fail with a named error everywhere instead of a NullReferenceException in some paths. LeagueApps no longer returns a null certificate and re-queries on every access. SignNowTest writes with File.WriteAllBytes, which truncates, so a leftover temp file can't leave trailing bytes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ROCK-9041: Fix ConnectionCards chop bounds and bitmap disposal; read via ReadContentBytes ChopImage now iterates exactly cols x rows cells and clamps each inset rectangle to the bitmap. The old step-until-edge loop added a partial column/row when the size wasn't evenly divisible, and Clone threw OutOfMemoryException. All bitmaps are disposed. Drops the private ReadAllBytes helper and corrects the RotateImage comment (the old code leaked the provider stream and Image; it did not upload a zero-byte blob). README updated for provider-agnostic reads and the save-hook resize caveat. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ROCK-9041: Read SignNowCreate's rendered PDF via ReadContentBytes Keeps #321's temp-directory handling (finally cleanup, SDK error handling) and only replaces the ContentStream.CopyTo read with the DevLib helper, so the cached stream on the tracked BinaryFile is never disposed or left open. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ROCK-9041: Stop persisting Wellright signing keys per request; read via ReadContentBytes The PFX was imported with PersistKeySet on every page load and never disposed, leaving a new private-key file in the machine key store each time. It now imports without PersistKeySet and disposes the certificate after signing. Exportable stays because CertificateUtility exports the key. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ROCK-9041: Document storage-provider support in PDF and SafetyAndSecurity READMEs Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ROCK-9041: Reject bad chop grids and zero-page PDFs in ConnectionCards instead of losing the scan ChopImage silently skipped cells when rows/columns made a cell 4px or smaller, so the block deleted the source scan, showed success and launched no workflows (the Rows/Columns NumberUpDowns had no bounds). It now throws InvalidOperationException for fewer than 1 row/column or cells of 4px or less, before anything is saved. The block shows the message and keeps the scan. Rows/Columns get Minimum="1". The no-op Rectangle.Intersect is removed. ConvertPDFToImage returns null for a PDF with no pages (an empty BinaryFile made the save hook throw on its null MimeType), and the block shows an error. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ROCK-9041: Document that ReadContentBytes reads stored bytes (save before reading) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ROCK-9041: Document ConnectionCards grid validation and zero-page handling Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> * ROCK-9041 Address review findings on storage-provider fixes - ReadContentBytes: decide "unsaved" by Id == 0, not StorageProvider != null, so a new file that already has a storage type is read from ContentStream. - SignNowDownload: a Document attribute whose file type no longer exists is an error, not a silent fallback to the Default type; resolve the destination before downloading; accept the %PDF- header anywhere in the first 1024 bytes. - SignNowCreate: report a missing document, temp-file write failures, and failed signer/token/invite calls as error messages instead of throwing. - CertificateUtility: dispose the RSACryptoServiceProvider per request. - SignNowTest: per-request temp directory removed in a finally. - ConnectionCardEntry: keep the scan and show a message on any ChopImage failure. - FaceCrop: dispose the rotated source and target bitmaps. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ROCK-9041 Update plugin READMEs for the DevLib dependency and behavior changes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

Summary
Remediates the remaining plugin findings from the storage-provider audit on ROCK-9041. Companion to #321 (
SignNowDownload, audit CRITICAL #1) and secc/Rock#18. #18 is closed rather than merged because SECC cherry-picks it into our Rock build until it's fixed upstream. It fixes the write side (AzureBlobStoragenot disposing the upload stream). This PR covers the read side and the cached-stream reuse, which #18 doesn't touch. Both are needed.Merge order: #321 is merged; this branch is rebased on
hotfix-1.16.12after it.Every read now goes through one helper,
BinaryFile.ReadContentBytes()in org.secc.DevLib. It opens a fresh storage-provider stream (StorageProvider.GetContentStream), copies the bytes into memory, and disposes the stream. Consumers (iText, GDI+, Ghostscript, temp files) get aMemoryStreamor a byte array. Provider streams are never left open, and code never assumes the Database provider. A missing or empty file throws anInvalidOperationExceptionthat names the file.Why a helper instead of
using ( var s = binaryFile.ContentStream )The first revision of this PR wrapped
ContentStreaminusing. That breaks on Azure.BinaryFile.ContentStreamcaches its stream and only re-fetches whenCanSeekis false. Azure'sLazyLoadingReadOnlyStreamstill reportsCanSeek = trueafterDispose. So any later read of the same trackedBinaryFilein the sameRockContextgot the disposed stream back and threwArgumentNullException. Examples: a second merge action on the same template, an email attachment later in the same workflow pass, orPDFCombineof one file with itself. The helper never touches the cached stream.Changes
org.secc.DevLib: new
Extensions/BinaryFileExtensions.cs(ReadContentBytes). README documents it.org.secc.ConnectionCards (
Utilities/ConnectionCardsUtilties.cs, audit CRITICAL #2, #3 and both HIGHs)ConvertPDFToImageandChopImagewrote content only toDatabaseData. Under a non-Database providerSaveContentnever ran, so cards were saved with no content. They now assignContentStreamandFileSize.ChopImagereadDatabaseData.Contentdirectly, which threw a NullReferenceException for Azure files. All three utilities now read viaReadContentBytes.BinaryFile, so the FileSystem provider's delete no longer hits an open handle.RotateImagenow disposes the provider stream and theImage. The old code leaked both.ChopImageiterates exactlycolsxrowscells; integer division keeps each inset cell inside the bitmap. The old step-until-edge loop added a partial row or column when the size didn't divide evenly (for example 1056 px with rows=5), andBitmap.ClonethrewOutOfMemoryException. All bitmaps are now disposed.ChopImagerejects a grid with fewer than 1 row or column, or with cells of 4 px or less, before anything is saved. The block shows the message and keeps the scan. Previously the cells were silently skipped, and the block deleted the scan, reported success and launched no workflows. Rows and Columns now haveMinimum="1".ConvertPDFToImagereturnsnullfor a PDF with no pages, and the block shows an error. The emptyBinaryFileit used to return made the save hook throw on its null MimeType.ReadAllByteshelper is gone.org.secc.PDF
PDFCombine.cs,PDFFormMerge.cs; org.secc.SafetyAndSecurity all five*Merge.cs: template and input bytes are read viaReadContentBytes, and iText gets aMemoryStream. PDF now references DevLib.org.secc.Imaging
AI/FaceCrop.cs: crops from an in-memory copy.org.secc.LeagueApps
Utilities/APIClient.cs: a missing or empty service-account file now throws a clear error. Before, it returned a null certificate, failed later insideX509Certificate2, and re-queried on every access.org.secc.SignNowWorkflow
SignNowCreate.cs: on top of #321's temp-directory cleanup and SDK error handling, the rendered PDF is read viaReadContentBytes. Now references DevLib.org.secc.Security
WellrightRedirect.ascx.cs: the signing PFX is no longer imported withPersistKeySet, which left a new private-key file in the machine key store on every page load. The certificate is disposed after signing.Exportablestays becauseCertificateUtilityexports the key.SignNowTest.ascx.cs: writes withFile.WriteAllBytes.File.OpenWritedidn't truncate, so a leftover longer temp file kept its trailing bytes.READMEs: ConnectionCards, PDF, SafetyAndSecurity and DevLib document provider-agnostic reads (addresses Copilot's review comments). The stale ConnectionCards
RotateImageobservation is removed.Not changed, and why
Attachments.ascx.cs(audit MEDIUM, cross-providerBinaryFileTypereassignment): Rock core'sBinaryFilesave hook already migrates content between providers when theBinaryFileTypenavigation property changes, which is what this code sets. No plugin change needed.GroupExtensionsController(audit LOW): the dispose half is covered by the cherry-picked ROCK-9041 Dispose ContentStream in AzureBlobStorage.SaveContent Rock#18.GetStatement.ashx,FontAwesomeSettings,ContributionStatementList: already dispose, viaSendFile'susing,ZipArchive, and an explicitusing.Deploy notes
ContentStream, Rock's save hook resizes them if the block's BinaryFileType sets a max width or height. Check that file type's limits before deploying. If it has limits, the sheet is downscaled before chopping.Ghostscript.NET1.2.1 reading the page count. Locally that fails on Ghostscript 9.54 and 10.08 (see Testing). My best guess is that Ghostscript 9.50 made-dSAFERthe default and broke the old wrapper, but I haven't verified that. Check the version underC:\Program Files\gson the prod nodes. If it's 9.50 or newer, PDF upload is already broken in production independent of this PR: before, it threw a NullReferenceException; now it shows the "no pages" message. The likely fix, a Ghostscript.NET upgrade or pinning the Ghostscript version, belongs on its own ticket.WellrightRedirectandSignNowTestare compiled at runtime and need the neworg.secc.DevLib.dllinRockWeb\Bin. The pipeline deploys it with the rest of the repo.Testing
DevLib, PDF, ConnectionCards, SignNowWorkflow, SafetyAndSecurity, Imaging and LeagueApps build clean (0 errors) against Rock 1.16.12.
Rebased onto
hotfix-1.16.12after ROCK-9041 Stop SignNowDownload leaking the temp PDF file handle #321's squash merge. The resulting tree is identical to the previously tested one.ConnectionCardEntry.ascx.cstype-checks with the same error profile asmaster. The only extra errors are 3 references to the newnbErrorcontrol, which is declared in the.ascxmarkup the probe doesn't compile.The two org.secc.Security
.ascx.csfiles aren't csproj Compile items. I type-checked them with Roslyn againstRockWeb\Binplus the new DevLib. The error profile is identical tomaster(only pre-existing missing-reference noise from the probe), so this PR adds no errors there.ConnectionCards local smoke test (2026-09-29, Rock 1.16.12 on LocalDB, IIS Express, this branch's DLLs and block): I drove the block with real WebForms postbacks, not by calling the code directly.
BinaryFileData,FileSizeset. Success message shown.OutOfMemoryException. It now yields exactly 10 cards.Not verified: converting a real PDF. The
Ghostscript.NET1.2.1 inRockWeb\BinreturnsPageCount 0for every PDF I tried, on both Ghostscript 10.08.0 and 9.54.0. That held via file path and via stream, for my hand-built PDFs and one written by Ghostscript itself, and with-dNOSAFERadded.gswin64creads the same files correctly. This is an existing environment issue, not a change in this PR; see Deploy notes.Still needs runtime checks: two PDFFormMerge actions on the same template in one workflow pass, PDFCombine of a file with itself, and ConnectionCards PDF → image conversion on a server whose Ghostscript works with Ghostscript.NET 1.2.1. Ideally run these on an Azure-backed file type.
🤖 Generated with Claude Code