Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/prompts/process-pr-feedback.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
description: read and process comments in a pr. Only appropriate to use in the rare situations where developer pre-review isn't needed.
---

Use the gh tool to determine the PR associated with the current branch. If you cannot find one, use the askQuestions tool to ask the user for a url.
Read the unresolved pr comments and either answer them or handle the problem they call out and then answer them.
When you answer, prefix your response with the name of your model, e.g. [hall9000]. I may ask you to "cycle" in which case you should make fixes and stop, or if you should also make a commit and push it and then wait 10 minutes for new comments and cycle. If I don't ask you to cycle, use the askQuestions tool to ask me if I should cycle or not.
59 changes: 51 additions & 8 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,37 @@
name: Build and Test

permissions:
contents: read

on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
workflow_dispatch:
inputs:
artifact-name-suffix:
description: "Suffix for artifact names"
required: false
default: ""
type: string
version-suffix:
description: "Optional SemVer prerelease suffix (example: test)"
required: false
default: ""
type: string
workflow_call:
inputs:
artifact-name-suffix:
description: "Suffix for artifact names"
required: false
default: ""
type: string
version-suffix:
description: "Optional SemVer prerelease suffix (example: test)"
required: false
default: ""
type: string
outputs:
version:
description: "The full version string generated"
Expand Down Expand Up @@ -55,7 +74,11 @@ jobs:

# Create version with run number (not run_id which is too large)
# Use run_number which is smaller and fits in NuGet version constraints
VERSION_SUFFIX="${{ inputs['version-suffix'] || '' }}"
FULL_VERSION="${BASE_VERSION}.${{ github.run_number }}"
if [ -n "$VERSION_SUFFIX" ]; then
FULL_VERSION="${FULL_VERSION}-${VERSION_SUFFIX}"
fi
echo "base_version=$BASE_VERSION" >> $GITHUB_OUTPUT
echo "full_version=$FULL_VERSION" >> $GITHUB_OUTPUT
echo "Base version: $BASE_VERSION"
Expand Down Expand Up @@ -85,28 +108,48 @@ jobs:
- name: Upload package artifacts to GitHub (Draft)
uses: actions/upload-artifact@v4
with:
name: nuget-packages-${{ steps.version.outputs.full_version }}${{ inputs.artifact-name-suffix }}
name: nuget-packages-${{ steps.version.outputs.full_version }}${{ inputs['artifact-name-suffix'] }}
path: nupkg/*.nupkg

- name: Upload compiled binaries to GitHub
uses: actions/upload-artifact@v4
with:
name: binaries-${{ steps.version.outputs.full_version }}${{ inputs.artifact-name-suffix }}
name: binaries-${{ steps.version.outputs.full_version }}${{ inputs['artifact-name-suffix'] }}
path: |
output/Release/*.dll
output/Release/*.pdb
output/Release/*.deps.json

create-release:
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
needs: build
permissions:
contents: write

steps:
- name: Download package artifacts
uses: actions/download-artifact@v4
with:
name: nuget-packages-${{ needs.build.outputs.version }}${{ inputs['artifact-name-suffix'] }}
path: release-artifacts/nupkg

- name: Download compiled binaries artifacts
uses: actions/download-artifact@v4
with:
name: binaries-${{ needs.build.outputs.version }}${{ inputs['artifact-name-suffix'] }}
path: release-artifacts/output

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.version.outputs.full_version }}
name: Release v${{ steps.version.outputs.full_version }}
tag_name: v${{ needs.build.outputs.version }}
name: Release v${{ needs.build.outputs.version }}
draft: true
prerelease: false
generate_release_notes: true
files: |
nupkg/*.nupkg
output/Release/*.dll
output/Release/*.pdb
output/Release/*.deps.json
release-artifacts/nupkg/*.nupkg
release-artifacts/output/*.dll
release-artifacts/output/*.pdb
release-artifacts/output/*.deps.json
47 changes: 41 additions & 6 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,22 @@ name: Publish to NuGet

on:
workflow_dispatch:
inputs:
version-suffix:
description: "Prerelease suffix for test packages (example: test)"
required: false
default: "test"
type: string

permissions:
contents: write

jobs:
build:
uses: ./.github/workflows/build.yml
with:
artifact-name-suffix: "-publish"
version-suffix: ${{ inputs['version-suffix'] }}

publish:
needs: build
Expand All @@ -16,7 +28,7 @@ jobs:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: nuget-packages-${{ needs.build.outputs.version }}
name: nuget-packages-${{ needs.build.outputs.version }}-publish
path: nupkg/

- name: Setup .NET
Expand All @@ -30,18 +42,41 @@ jobs:
ls -la nupkg/
echo ""
echo "Version: ${{ needs.build.outputs.version }}"
echo "Packages will be published as UNLISTED to NuGet.org"
echo "Target: https://api.nuget.org/v3/index.json"

- name: Validate NuGet API key secret is configured
env:
NUGET_API_KEY: ${{ secrets.SILLSDEV_PUBLISH_NUGET_ORG }}
run: |
if [ -z "$NUGET_API_KEY" ]; then
echo "Missing required repository secret: SILLSDEV_PUBLISH_NUGET_ORG"
exit 1
fi

- name: Publish to NuGet
- name: Publish package to NuGet
run: |
for file in nupkg/*.nupkg; do
echo "Publishing $file (unlisted)"
echo "Publishing $file"
dotnet nuget push "$file" --api-key ${{ secrets.SILLSDEV_PUBLISH_NUGET_ORG }} --source https://api.nuget.org/v3/index.json --skip-duplicate --no-service-endpoint
done

- name: Publish symbols package to NuGet
run: |
shopt -s nullglob
files=(nupkg/*.snupkg)
if [ ${#files[@]} -eq 0 ]; then
echo "No .snupkg files found; skipping symbols publish."
exit 0
fi

for file in "${files[@]}"; do
echo "Publishing symbols $file"
dotnet nuget push "$file" --api-key ${{ secrets.SILLSDEV_PUBLISH_NUGET_ORG }} --source https://api.nuget.org/v3/index.json --skip-duplicate --no-service-endpoint
done

- name: Publish success
run: |
echo "✅ Successfully published to NuGet (unlisted)!"
echo "✅ Successfully published to NuGet!"
echo "📦 Version: ${{ needs.build.outputs.version }}"
echo "🔗 Check your package at: https://www.nuget.org/packages/sillsdev.dotImpose/"
echo "ℹ️ Package is unlisted - users need the exact version number to install it"
echo "Install with: dotnet add package sillsdev.dotImpose --version ${{ needs.build.outputs.version }}"
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ _Resharper*
/.vs/
publish/
artifacts/

nupkg/local/
10 changes: 10 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"workbench.colorCustomizations": {
"statusBar.background": "#61dafb",
"statusBar.foreground": "#15202b",
"statusBarItem.hoverBackground": "#2fcefa",
"statusBarItem.remoteBackground": "#61dafb",
"statusBarItem.remoteForeground": "#15202b"
},
"peacock.color": "#61dafb"
}
28 changes: 27 additions & 1 deletion ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,33 @@ Each layout method has an `Id` property (for programmatic use) and an `EnglishLa

- Multiple PDF imposition layouts
- Support for right-to-left languages
- Crop marks for commercial printing
- Full-bleed aware imposition, crop marks.

## Full Bleed

DotImpose supports full-bleed workflows where the client application provides source PDFs with the expected PDF page boxes already defined (`TrimBox`, `BleedBox`, etc.).

### NullLayoutMethod

- Preserves source page content and source box geometry as-is (see note about Crop Marks below).
- `NullLayoutMethod(insetTrimboxMillimeters)` is deprecated.
- If a source page defines an explicit `TrimBox`, using non-zero `insetTrimboxMillimeters` now throws an error.
- If a source page does not define an explicit `TrimBox`, non-zero `insetTrimboxMillimeters` synthesizes a `TrimBox` by insetting from source bleed/media geometry. In other words, if you give it an A5 page and specify an insetTrimboxMillimeters value, you will get back pages where the TrimBox is smaller than A5.
- Preferred approach: use `NullLayoutMethod()` and provide explicit source `TrimBox`/`BleedBox` in your input PDF.

### SideFoldBookletLayouter

- Uses source `TrimBox` as the panel trim intent for each imposed page panel.
- Preserves source bleed/trim intent so cutoff matches source expectations.
- Does not impose a special fold-edge clipping rule on your behalf.
- If you want content to print all the way to the fold, define source boxes accordingly (for example by setting trim/bleed so that edge is treated as printable).
- If you want content to stop at the fold, define source boxes so that edge trims there.

The output page boxes describe the final imposed sheet, while source box definitions remain the authority for panel-level cutoff intent.

### Crop Marks

When crop marks are enabled, DotImpose adds marks outside the final trim area. Crop marks do not change trim or bleed calculations; if needed, DotImpose will expand the `MediaBox` to make room for them.

## Building

Expand Down
1 change: 1 addition & 0 deletions src/DotImpose.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>pdf;imposition;layout;booklet;calendar</PackageTags>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<InformationalVersion>$(Version)</InformationalVersion>
<!-- Generate XML documentation for IntelliSense -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- Include README in NuGet package -->
Expand Down
29 changes: 29 additions & 0 deletions src/DotImposeRuntimeInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Reflection;

namespace DotImpose
{
/// <summary>
/// Provides runtime identity details for the loaded dotImpose assembly.
/// </summary>
public static class DotImposeRuntimeInfo
{
/// <summary>
/// Returns the loaded assembly's informational version.
/// </summary>
public static string GetInformationalVersion()
{
var assembly = typeof(DotImposeRuntimeInfo).Assembly;
return assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
?? assembly.GetName().Version?.ToString()
?? "unknown";
}

/// <summary>
/// Returns the loaded assembly file path.
/// </summary>
public static string GetAssemblyPath()
{
return typeof(DotImposeRuntimeInfo).Assembly.Location;
}
}
}
3 changes: 3 additions & 0 deletions src/InternalsVisibleTo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("sillsdev.dotImpose.Tests")]
33 changes: 25 additions & 8 deletions src/LayoutMethods/CalendarLayouter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,15 @@ protected override void LayoutInner(PdfDocument outputDocument, int numberOfShee
if (2 * idx <= _inputPdf.PageCount) //prevent asking for page 2 with a single page document (JH Oct 2010)
{
//Left side of back
DrawSuperiorSide(gfx, 2 * idx);
DrawSuperiorSide(gfx, 2 * idx, graphicsAreRotated180: true);
}

//Right side of the Back
if (vacats > 0) // Skip if right side has to remain blank
vacats -= 1;
else
{
DrawInferiorSide(gfx, numberOfPageSlotsAvailable + 1 - 2 * idx);
DrawInferiorSide(gfx, numberOfPageSlotsAvailable + 1 - 2 * idx, graphicsAreRotated180: true);
}
}
}
Expand All @@ -78,30 +78,47 @@ public override bool GetIsEnabled(XPdfForm inputPdf)

/// With the portrait, left-to-right-language mode, this is the Right side.
/// With the landscape, this is the bottom half.
private void DrawInferiorSide(XGraphics gfx, int pageNumber /* NB: page number is one-based*/)
private void DrawInferiorSide(XGraphics gfx, int pageNumber /* NB: page number is one-based*/, bool graphicsAreRotated180 = false)
{
_inputPdf.PageNumber = pageNumber;
XRect box;
if (_inputPdf.PixelWidth > _inputPdf.PixelHeight)//landscape calendar
box = new XRect(0, _paperHeight / 2, _paperWidth, _paperHeight / 2);
else
box = new XRect(LeftEdgeForInferiorPage, 0, _paperWidth / 2, _paperHeight);
gfx.DrawImage(_inputPdf, box);

if (graphicsAreRotated180)
DrawPageUsingSourceTrimIntent(gfx, pageNumber, box, MapRotatedGraphicsTrimBoxToPageTrimBox(box));
else
DrawPageUsingSourceTrimIntent(gfx, pageNumber, box);
}

/// <summary>
/// With the portrait, left-to-right-language mode, this is the Left side.
/// With the landscape, this is the top half.
/// </summary>
private void DrawSuperiorSide(XGraphics gfx, int pageNumber)
private void DrawSuperiorSide(XGraphics gfx, int pageNumber, bool graphicsAreRotated180 = false)
{
_inputPdf.PageNumber = pageNumber;
XRect box;
if (_inputPdf.PixelWidth > _inputPdf.PixelHeight)//landscape calendar
box = new XRect(0, 0, _paperWidth, _paperHeight / 2);
else
box = new XRect(LeftEdgeForSuperiorPage, 0, _paperWidth / 2, _paperHeight);
gfx.DrawImage(_inputPdf, box);

if (graphicsAreRotated180)
DrawPageUsingSourceTrimIntent(gfx, pageNumber, box, MapRotatedGraphicsTrimBoxToPageTrimBox(box));
else
DrawPageUsingSourceTrimIntent(gfx, pageNumber, box);

}

private XRect MapRotatedGraphicsTrimBoxToPageTrimBox(XRect graphicsTrimBox)
{
// Calendar back pages rotate graphics by 180 degrees around the trim page.
return new XRect(
_paperWidth.Point - (graphicsTrimBox.X + graphicsTrimBox.Width),
_paperHeight.Point - (graphicsTrimBox.Y + graphicsTrimBox.Height),
graphicsTrimBox.Width,
graphicsTrimBox.Height);

}

Expand Down
6 changes: 2 additions & 4 deletions src/LayoutMethods/CutLandscapeLayouter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,14 @@ protected override void LayoutInner(PdfDocument outputDocument, int numberOfShee

private void DrawBottom(XGraphics gfx, int pageNumber /* NB: page number is one-based*/)
{
_inputPdf.PageNumber = pageNumber;
XRect box = new XRect(0, _paperHeight / 2, _paperWidth, _paperHeight / 2);
gfx.DrawImage(_inputPdf, box);
DrawPageUsingSourceTrimIntent(gfx, pageNumber, box);
}

private void DrawTop(XGraphics gfx, int pageNumber)
{
_inputPdf.PageNumber = pageNumber;
XRect box = new XRect(0, 0, _paperWidth, _paperHeight / 2);
gfx.DrawImage(_inputPdf, box);
DrawPageUsingSourceTrimIntent(gfx, pageNumber, box);
}

}
Expand Down
3 changes: 1 addition & 2 deletions src/LayoutMethods/Folded8Up8PageBookletLayouter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,13 @@ protected override void LayoutInner(PdfDocument outputDocument, int numberOfShee
private void Draw8UpPageFor8PageBooklet(XGraphics gfx, int pageNumber, double xorigin, double yorigin)
{
var state = gfx.Save();
_inputPdf.PageNumber = pageNumber;
var box = new XRect(xorigin, yorigin, _paperWidth / 4, _paperHeight / 2);
if (yorigin == 0)
{
var pagePoint = new XPoint(xorigin + _paperWidth / 8, yorigin + _paperHeight / 4);
gfx.RotateAtTransform(180, pagePoint);
}
gfx.DrawImage(_inputPdf, box);
DrawPageUsingSourceTrimIntent(gfx, pageNumber, box);
gfx.Restore(state);
}
}
Expand Down
Loading