Set spreadsheet export image resolution to 96dpi (BL-16529) - #8197
Set spreadsheet export image resolution to 96dpi (BL-16529)#8197StephenMcConnel wants to merge 6 commits into
Conversation
This is simpler and more reliable than trying to compensate for whatever resolution the exporting machine is using by adjusting the image size.
|
[Claude Opus 5] Consulted Devin on 2026-08-12 14:53 UTC-6 up to commit Devin found no bugs and no Investigate flags — nothing to mirror as review threads. It did raise 4 informational items, none of them action items:
CI ( |
…6529) The two export-level tests cannot protect this fix on our build machines: on a display at 100% scaling a GDI+ bitmap already carries 96dpi, so they pass whether or not the exporter stamps the resolution, and every build machine is at 100%. Deleting the one-line stamp would therefore go unnoticed until it reappeared as the original bug on a high-DPI machine -- which is what made this issue take three rounds to pin down. Extract the save step into SaveThumbnailForEmbedding(Bitmap, Stream) and test it directly with a bitmap deliberately marked 192dpi, which is what it would really receive on a 200%-scaled display. That assertion has the same force on every machine. Verified by commenting out the SetResolution call: the new test fails (192 vs 96) while both export-level tests still pass. No behavior change -- the same operations in the same order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-16529) SetResolution works in place, and Bitmap.Save offers no way to override the resolution as it writes, so the method's real contract is "save, and also change my argument". That is harmless for the freshly-created thumbnails the exporter passes it, but it would surprise a future caller handing over a shared or cached bitmap. Say so in the doc comment. (Devin informational.) Comment only; no behavior change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…529) The row height converted thumbnail pixels to points against a literal 96, the same number we stamp on the thumbnail, but written out independently. Changing the stamped resolution would then have left every row sized for a resolution the images no longer had -- a silent desynchronization of exactly the kind that made this issue hard to pin down. Point both at standardImageDpi and say so where it is declared. (Devin informational.) The constant becomes int so the conversion stays integer arithmetic, bit for bit as before: int * int / const int is integer division, and SetResolution takes the int by implicit conversion to float. No behavior change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
[Claude Opus 5] Consulted Devin on 2026-08-12 (final preflight cycle) up to commit No bugs and no Investigate flags — that is now three consecutive Devin runs clean (
The rest needed no action:
CI ( |
Spreadsheet export embedded thumbnails whose size depended on the display of the machine doing the export. The root cause is not the pixel count but the thumbnail's DPI metadata.
ImageUtils.CreateScaledBitmapbuilds the thumbnail with a plain GDI+new Bitmap(w, h), which inherits the screen's DPI, andSave(..., Jpeg)writes that into the JPEG header. Since Bloom became PerMonitorV2 DPI aware (Program.cs), that is 192 on a 200%-scaled display instead of 96. EPPlus derives a picture's physical extent from its pixel size divided by the image's own resolution, so every thumbnail was laid out at half its intended size — literally the reported "images only take up half the column width".Measured from the three exports of the same book attached to BL-16529:
Column width was byte-identical in all three, so the column was never involved. The earlier width fix (#8094) doubled the pixel count, which cancels a 192-dpi error only when the scale factor is exactly 200; in general it gave
150 px * scaleFactor / 200, which is why exports below 200% still came out too small.That also explains why this reproduced for some people and not others: the two errors cancel exactly whenever the bitmap's DPI and
GetScaleFactorForMonitor's answer agree (96x150, 120x187, 192x300 all yield a 150 px extent). Where the two independent DPI sources disagree — mixed-scaling multi-monitor setups, or a scale change without signing out — they don't cancel.The change
SpreadsheetIO: stamp the thumbnail with 96dpi before saving, and drop the DPI compensation entirely — the width up-scaling, the row-height division, and theGetScalingFactorForPrimaryMonitorcall. Export now queries no monitor and produces the same file on every machine, matching 6.4.WindowsMonitorScaling: remove the now-unusedGetScalingFactorForPrimaryMonitor,MonitorFromPointandPOINT. The Problem Report screenshot path is untouched.Tests: new
Export_Thumbnail_IsStamped96Dpi_SoLayoutDoesNotDependOnTheDisplayasserts the embedded thumbnail is 96dpi and that the picture's extent, read from the raw drawing XML, is the full 150 px. The existing row-height test is now two-sided (its previous doc comment described the DPI compensation that this removes).Tests:
SaveThumbnailForEmbeddingis a separate method purely so a test can prove the stamping happens.SaveThumbnailForEmbedding_StampsStandard96Dpi_WhateverResolutionItWasGivenhands it a bitmap deliberately marked 192dpi -- what it would really receive on a 200%-scaled display -- so the assertion carries the same force on every machine, including CI at 100%.Verified with teeth: stamping 192dpi deliberately makes the new test fail with an extent of exactly 75 px, reproducing the reported file; at 96dpi it is 150 px. This gives the
>100%display path its first test coverage — the previous test would have passed with the DPI code deleted outright.Note the assertions can only fail on a machine scaled above 100%, so CI (at 100%) will not catch a regression here; that limitation is documented in the test.
Ref: https://issues.bloomlibrary.org/youtrack/issue/BL-16529
Devin review
This change is